npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rhombussystems/react

v2.0.1

Published

React components for Rhombus camera streaming (DASH via Dash.js and realtime H.264 via WebSocket + WebCodecs)

Readme

Rhombus React SDK — @rhombussystems/react

React + TypeScript components for embedding Rhombus camera video in your own app. The SDK streams over two transports — MPEG-DASH (Dash.js) and low-latency H.264 over WebSocket (WebCodecs) — and ships a unified drop-in player that combines them with a full set of player controls.

Your Rhombus API key never ships to the browser. Everything is built around short-lived federated session tokens minted by your backend (see Authentication).

Version: this guide tracks @rhombussystems/react 2.0.0. React 18+.


Contents


Install

npm install @rhombussystems/react
# or: yarn add @rhombussystems/react  /  pnpm add @rhombussystems/react
  • react and react-dom (>= 18) are peer dependencies — install them in your app.
  • **dashjs** is bundled (used for DASH playback) — you do not install it separately.
  • The realtime/canvas path uses the browser WebCodecs VideoDecoder (Chrome, Edge, Safari 16.4+; Firefox H.264 is still limited) — no extra dependency.

Quick start

A complete live/VOD player with controls, a timeline, zoom, snapshot, and clip export — from a single cameraUuid:

import { RhombusPlayer } from "@rhombussystems/react";

export function CameraView() {
  return (
    <RhombusPlayer
      cameraUuid="YOUR_CAMERA_UUID"
      apiOverrideBaseUrl="https://your-api.example.com" // proxy mode (recommended)
      style={{ height: 480 }}
    />
  );
}

⚠️ Server setup is required. The SDK calls your origin for a token. Your server must expose POST /api/federated-token (the default path) or set paths.federatedToken to your route. Built-in Save Clip additionally needs a few proxy routes. See the Backend contract.

Prefer to compose your own layout? Drop down to the individual building blocks — each has a deep-dive section further down the page:

  • RhombusBufferedPlayer — MPEG-DASH live & VOD on a real <video> element; native pause/seek, widest browser support.
  • RhombusRealtimePlayer — sub-second live H.264 over WebSocket, decoded with WebCodecs onto a <canvas> (live only).

Choosing a component

| Component | Transport | Live latency | Live | Past (VOD) | Controls | | --------------------------- | --------------------------------------------------------------------- | --------------- | ---- | ---------- | ---------------------- | | **RhombusPlayer** | both — realtime canvas for live, DASH for VOD, switched automatically | sub-second live | ✅ | ✅ | ✅ full bar + ref API | | **RhombusBufferedPlayer** | MPEG-DASH (Dash.js) on a <video> | ~few seconds | ✅ | ✅ | native <video> | | **RhombusRealtimePlayer** | H.264 / WebSocket → WebCodecs → <canvas> | sub-second | ✅ | ❌ | none (always live) | | **Timeline** | none — a canvas scrubber you pair with any video | — | — | — | seek UI only |

Rule of thumb: reach for **RhombusPlayer** first — it's the drop-in. Drop down to the individual players when you want to compose your own layou or have a single source of truth for your playback time and playback state (ex: video walls).


RhombusPlayer — the unified player

RhombusPlayer composes RhombusRealtimePlayer and RhombusBufferedPlayer behind one interface and adds player-level controls: play/pause, go-live, rewind, playback speed, digital zoom + pan, snapshot, an event-aware timeline, and save clip. It automatically switches between Live and VOD as the user interacts with the timeline and Go-Live button.

import { RhombusPlayer } from "@rhombussystems/react";

<RhombusPlayer
  cameraUuid="YOUR_CAMERA_UUID"
  apiOverrideBaseUrl="https://your-api.example.com"
  showLiveTypeSwitcher            // optional Console-style Realtime/Buffered + quality menu
  saveClip={{ defaultTitle: "Door cam" }}
  timeline={{ fetchSeekPoints: true }}   // 24h day window by default, ±12h chevrons
  onModeChange={(mode, atMs) => console.log(mode, new Date(atMs))}
/>

How Live ⇄ VOD switching works

Switching is a pure function of time vs. now:

  • Live uses the realtime transport by default (RhombusRealtimePlayer, WebCodecs canvas, sub-second). It auto-falls back to buffered DASH when WebCodecs is unavailable.
  • Pause, rewind, change speed, or seek into the past drops the player into VOD (RhombusBufferedPlayer anchored on a manifest window containing the target time).
  • Go Live (or seeking within liveEdgeToleranceSec of now) returns to the live edge.

Only one transport is mounted at a time, so a switch costs one brief reconnect (no double bandwidth). Seeking within the loaded VOD window is instant (native <video> seek); seeking outside it loads a fresh manifest window.

RhombusPlayer props

Every prop RhombusPlayer accepts. Only cameraUuid is required; everything else is optional. (The auth / endpoint / resilience props are the shared base props common to all players.)

| Prop | Type | Required | Default | Notes | | ---------------------------- | ----------------------------------------------------- | -------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | cameraUuid | string | ✅ | — | Camera UUID from Rhombus. Safe in the browser. | | connectionMode | "wan" | "lan" | — | "wan" | Which getMediaUris URIs to use. See WAN vs LAN. | | apiOverrideBaseUrl | string | — | — | Base for the token and media requests (proxy mode). Required for built-in Save Clip. When omitted, media is fetched directly from Rhombus. | | rhombusApiBaseUrl | string | — | https://api2.rhombussystems.com/api | Rhombus REST base when apiOverrideBaseUrl is omitted. | | paths | { federatedToken?, mediaUris?, footageSeekpoints? } | — | see backend | Override route paths. | | federatedSessionToken | string | — | — | Supply & rotate your own token; the SDK skips its token endpoint. | | tokenDurationSec | number | — | 86400 | Requested token TTL (SDK-managed mode). | | headers | HeadersInit | — | — | Static headers for the token request (+ media when apiOverrideBaseUrl set). | | getRequestHeaders | () => HeadersInit | Promise<…> | — | — | Async headers merged after headers. | | maxRetryIntervalMs | number | — | 30000 | Auto-recovery backoff ceiling. 0 disables. | | stallTimeoutMs | number | — | 12000 | Stall watchdog. 0 disables. | | liveTransport | "realtime" | "buffered" | — | "realtime" | Live transport. Controllable. Auto-falls back to buffered without WebCodecs. | | videoFit | "contain" | "cover" | "fill" | "auto" | — | "auto" | How the video fills its area. Controllable; built-in "videoFit" control changes it. See Video display / fit. | | onVideoFitChange | (fit) => void | — | — | Fired when the video-display fit changes. | | playing | boolean | — | — | Controlled play/pause. Omit = uncontrolled (starts playing). Pair with onPlayingChange. See Controlled vs. imperative. | | playbackRate | number | — | — | Controlled VOD speed (no-op while live). Pair with onPlaybackRateChange. | | zoom | number (1–4) | — | — | Controlled digital zoom. Pair with onZoomChange. | | positionMs | number (epoch ms) | — | — | Controlled playhead — seeks when its value changes; mode is derived (near now ⇒ live). Mirror onProgress/onSeek for two-way binding. | | showLiveTypeSwitcher | boolean | — | false | Render the Console-style Realtime/Buffered + quality menu in the bar. | | realtimeStreamQuality | "HD" | "SD" | — | "HD" | Live quality when the resolved transport is realtime. | | bufferedStreamQuality | "HIGH" | "MEDIUM" | "LOW" | — | "HIGH" | DASH quality for buffered live + VOD. | | applyBufferedStreamQuality | boolean | — | true | Set false to omit the _ds downscale. | | initialMode | "live" | "vod" | — | "live" | Start live or jump straight into the past. | | initialStartTimeMs | number (epoch ms) | — | — | Anchor used when initialMode="vod". | | vodWindowSec | number | — | 7200 | Length of the VOD manifest window the SDK requests. | | defaultRewindSec | number | — | 15 | Step used by the Rewind button / rewind(). | | liveEdgeToleranceSec | number | — | 5 | A seek within this many seconds of now counts as live. | | autoGoLiveAtEdge | boolean | — | false | Auto-return to live when VOD playback catches up to the edge. | | controls | RhombusPlayerControl[] | — | undefined | Which built-in controls to render. Leaving it undefined renders every control; [] = headless. There is no "all" value. See below. | | classNames | RhombusPlayerClassNames | — | — | Per-slot class names for the bar. See Styling. | | renderControls | (api, state) => ReactNode | — | — | Replace the bar entirely (timeline still renders). | | saveClip | RhombusSaveClipConfig | — | — | Built-in clip export config. See Save Clip. | | timeline | RhombusPlayerTimelineConfig | — | — | Timeline/scrubber config. See Timeline. | | className / style | string / CSSProperties | — | — | Applied to the player's root element. | | onReady | () => void | — | — | First underlying transport became ready. | | onError | (error: Error) => void | — | — | Token / media / setup failure. | | onRecoveryAttempt | (attempt, error) => void | — | — | Fires on each auto-recovery retry. | | onModeChange | (mode, atWallClockMs) => void | — | — | Fired on every Live ⇄ VOD transition. | | onTransportChange | (transport) => void | — | — | Resolved live transport changed (incl. WebCodecs fallback). | | onSeek | (wallClockMs, mode) => void | — | — | A seek happened. | | onProgress | (wallClockMs, mode) => void | — | — | Throttled playback progress (~4Hz VOD / ~1Hz live). Use to mirror a controlled positionMs. | | onPlayingChange | (playing) => void | — | — | Play/pause state changed. | | onPlaybackRateChange | (rate) => void | — | — | Playback speed changed. | | onSnapshot | (RhombusSnapshotResult) => void | — | — | A snapshot was captured. | | onZoomChange | (zoom, panX, panY) => void | — | — | Zoom/pan changed. | | onClipRangeSelect | (RhombusClipRange) => void | — | — | User selected a clip range (fires regardless of built-in export). | | onClipExport | (RhombusClipExportStatus) => void | — | — | Built-in clip export progress/result. |

Controlled, uncontrolled & imperative

You don't have to choose one approach. Props, the ref, and the built-in control bar all read and write the same internal state, so they work together and stay in sync — drive some aspects declaratively and others imperatively, or let users click the built-in bar; every path fires the matching on*Change callback so your state can follow. (The only caveat is the standard React one: see "Notes" below.)

  1. Controlled value props — drive a steady-state value declaratively. Each is optional: omit it and the player owns it internally (uncontrolled, seeded by initial*/defaults); provide it (and update it from the matching on*Change) and it becomes the source of truth. The built-in controls and the ref still work — in controlled mode they fire on*Change so your state updates.

| Prop | Callback | | --------------- | ---------------------- | | playing | onPlayingChange | | playbackRate | onPlaybackRateChange | | zoom | onZoomChange | | liveTransport | onTransportChange | | videoFit | onVideoFitChange |

  1. Controlled playheadpositionMs (epoch ms). It seeks when its value changes (the player derives live vs. VOD: within liveEdgeToleranceSec of now ⇒ live in the current transport, else VOD — there is no mode prop). The player still advances on its own; for a two-way binding, mirror onProgress (throttled) and/or onSeek back into positionMs:
  2. Imperative actions — one-shot commands on the ref. Some are just sugar over a declarative prop (use whichever you prefer); two are strictly imperative because they return a value / run an async side-effect and have no meaningful "state" to bind:

| ref method | Declarative equivalent | | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | play() / pause() | playing | | setPlaybackRate(r) | playbackRate | | zoomIn() / zoomOut() / setZoom() / resetZoom() | zoom | | setLiveTransport(t) | liveTransport | | seekTo(ms) / rewind(s) / goLive() | positionMs (set to the time / now − s / now) | | **snapshot()** | none — strictly imperative (returns the captured frame). | | **startClipExport(range?, opts?)** | none — strictly imperative (clip capture; runs the async render, returns status). Clip range selection is the built-in UI / onClipRangeSelect, but the export itself is a command. |

So: everything that has a steady-state value is available as a controlled prop; the only things that are ref-only are **snapshot()** and **startClipExport()** (and you'd typically also reach for getState() imperatively).

Notes (controlled semantics): when you provide a controlled prop, you own it — if you ignore its on*Change, the prop and the player can diverge until the next prop change (standard React controlled behavior; e.g. the built-in Pause button fires onPlayingChange(false), and if you don't update your playing state the player re-asserts your prop). getState() always returns the effective values regardless of how you drive it, and the ref works in controlled or uncontrolled mode.

Imperative handle (ref)

Pass a ref to drive the player programmatically. The built-in control bar uses this exact API internally, so anything the buttons do, you can do too.

import { useRef } from "react";
import { RhombusPlayer, type RhombusPlayerHandle } from "@rhombussystems/react";

function Controlled() {
  const player = useRef<RhombusPlayerHandle>(null);
  return (
    <>
      <RhombusPlayer ref={player} cameraUuid="…" apiOverrideBaseUrl="https://api.example.com" />
      <button onClick={() => player.current?.pause()}>Pause</button>
      <button onClick={() => player.current?.goLive()}>Go live</button>
      <button onClick={() => player.current?.rewind(30)}>« 30s</button>
      <button onClick={() => player.current?.seekTo(Date.now() - 3_600_000)}>1h ago</button>
      <button onClick={async () => {
        const shot = await player.current?.snapshot();
        if (shot) downloadDataUrl(shot.dataUrl, "frame.png");
      }}>Snapshot</button>
    </>
  );
}

| Method | Description | | --------------------------------------------- | ---------------------------------------------------------------- | | play() / pause() | Play / pause. Pausing live drops into a frozen VOD frame. | | goLive() | Return to the live edge (restores the live transport). | | seekTo(wallClockMs) | Seek to an absolute time (epoch ms); auto-switches Live ⇄ VOD. | | rewind(seconds?) | Jump back seconds (default defaultRewindSec). | | setPlaybackRate(rate) | VOD only; ignored while live. | | zoomIn(step?) / zoomOut(step?) | Digital zoom (1×–4×). | | setZoom(zoom, panX?, panY?) / resetZoom() | Set zoom + pan directly / reset to 1×. | | snapshot() | Promise<RhombusSnapshotResult> — capture the current frame. | | setLiveTransport("realtime" | "buffered") | Switch transport (clamps to buffered without WebCodecs). | | startClipExport(range?, options?) | Promise<RhombusClipExportStatus> — export a clip (proxy mode). | | getState() | Current RhombusPlayerState snapshot. |

Observable state

renderControls(api, state) receives — and getState() returns — a RhombusPlayerState:

type RhombusPlayerState = {
  cameraUuid: string;
  mode: "live" | "vod";
  liveTransport: "realtime" | "buffered";  // resolved (may have fallen back)
  playing: boolean;
  playbackRate: number;
  currentWallClockMs: number | null;        // ≈ Date.now() while live
  zoom: number;
  isAtLiveEdge: boolean;
  canSaveClip: boolean;                      // built-in export available (proxy mode)
  clipSelection: { startMs: number; endMs: number } | null; // current clip selection, or null
  clipExport?: RhombusClipExportStatus;      // in-progress / finished export
};

Choosing which controls render

controls is a list of RhombusPlayerControl. It's exported both as a string union and as a runtime constant (RhombusPlayerControl.Play, etc.), so use plain strings or named members — whichever you prefer:

"play" | "goLive" | "rewind" | "speed" | "zoom" | "snapshot" | "saveClip" | "timeline" | "liveType" | "videoFit"
import { RhombusPlayer, RhombusPlayerControl } from "@rhombussystems/react";

// All controls — omit the prop entirely:
<RhombusPlayer cameraUuid="…" />

// A subset — plain strings:
<RhombusPlayer cameraUuid="…" controls={["play", "timeline"]} />

// …or the named constant (autocompletes, refactor-safe):
<RhombusPlayer cameraUuid="…" controls={[RhombusPlayerControl.Play, RhombusPlayerControl.Timeline]} />

// Headless — no built-in UI at all; drive everything through the ref:
<RhombusPlayer ref={player} cameraUuid="…" controls={[]} />

Video display / fit

Cameras are usually 16:9; when the player box isn't, you get letter/pillar-boxing. The videoFit prop controls how the footage fills its area, mirroring the Rhombus Console video-wall "Video Display" options:

| videoFit | Console label | Behavior | | -------------------- | -------------------- | ------------------------------------------------------------------------- | | "auto" (default) | Auto-Size | The player box takes the video's aspect ratio — no bars, no cropping. | | "contain" | Default Aspect Ratio | Full frame, letter/pillar-boxed (object-fit: contain). | | "cover" | Full View Cropped | Fills the box, crops overflow (object-fit: cover). | | "fill" | Stretch to Fit | Distorts to fill, no cropping (object-fit: fill). |

There's a built-in video-display control in the bar (the "videoFit" control) so users can switch between these live; it fires onVideoFitChange. You can also drive it as a controlled prop:

<RhombusPlayer cameraUuid="…" videoFit="cover" onVideoFitChange={setFit} />

**"auto" sizes by width:** the player measures the video's intrinsic aspect ratio and sets the stage's aspect-ratio (height is derived), so give the player a width and don't impose a fixed height in that mode. The other three modes fill whatever box you give it.

For the low-level RhombusBufferedPlayer / RhombusRealtimePlayer, set object-fit yourself via videoProps.style / canvasProps.style.

Styling the controls

Three options, least → most custom:

1. Plain CSS overrides. The bar uses stable class names, and the SDK ships its defaults as a zero-specificity :where() stylesheet injected once at runtime. Because every default selector sits inside :where() (specificity 0,0,0), your CSS always wins — no !important, no import, regardless of load order:

| Element | class | | ------------------ | ---------------------------------------------------------------------------- | | the bar | rhombus-player-controls | | every button | rhombus-player-btn (active: [data-active="true"]; disabled: :disabled) | | speed <select> | rhombus-player-speed | | quality <select> | rhombus-player-quality | | live-type group | rhombus-player-livetype | | clip group | rhombus-player-clip | | clip status text | rhombus-player-clip-status | | timeline wrapper | rhombus-player-timeline |

.rhombus-player-controls { background: #fff; color: #111; gap: 12px; }
.rhombus-player-btn { background: #0a7; border-color: #0a7; border-radius: 999px; }
.rhombus-player-btn[data-active="true"] { outline: 2px solid #0a7; }

2. classNames prop — attach your own class per slot (Tailwind, CSS-modules, design systems). Appended to the SDK's class on that element:

<RhombusPlayer
  cameraUuid="…"
  classNames={{ controls: "flex gap-3 p-2 bg-white", button: "btn btn-sm", clip: "ml-auto" }}
/>

3. renderControls — replace the bar entirely (the timeline still renders above it) and build your own buttons against the imperative api:

<RhombusPlayer
  cameraUuid="…"
  renderControls={(api, s) => (
    <div className="my-bar">
      <button onClick={() => (s.playing ? api.pause() : api.play())}>
        {s.playing ? "Pause" : "Play"}
      </button>
      {s.mode === "vod" && <button onClick={() => api.goLive()}>Go live</button>}
      <button onClick={() => api.rewind()}>« 15s</button>
      <button disabled={s.mode === "live"} onClick={() => api.setPlaybackRate(2)}>2×</button>
      <button onClick={() => api.zoomIn()}>+</button>
      <button onClick={() => void api.snapshot()}>Snapshot</button>
    </div>
  )}
/>

renderControls is fully optional — omit it to keep the built-in bar. For total control, combine controls={[]} (no bar) with the ref handle and your own layout.

Snapshots

The Snapshot tool captures the current frame and hands the image data back to you — it does not auto-download and there is no target container/ref to render into. It works in both modes (the realtime canvas and the MSE-fed DASH <video> are both untainted, so toDataURL / toBlob succeed) and returns a RhombusSnapshotResult:

type RhombusSnapshotResult = {
  dataUrl: string;   // PNG data: URL
  blob: Blob | null; // PNG blob (null only if toBlob is unavailable)
  wallClockMs: number;
  mode: "live" | "vod";
  width: number;
  height: number;
};

You receive it two ways — both deliver the same result, including for the built-in Snapshot button:

// 1) Callback — fires for the built-in button AND for api.snapshot()
<RhombusPlayer cameraUuid="…" onSnapshot={(shot) => setPreview(shot.dataUrl)} />

// 2) Imperative — capture on demand and use the returned result
const shot = await playerRef.current!.snapshot();

The SDK never downloads or displays the image itself — render it (<img src={shot.dataUrl} />), upload shot.blob, or trigger a download yourself:

const shot = await playerRef.current!.snapshot();
const a = document.createElement("a");
a.href = shot.dataUrl;                          // or URL.createObjectURL(shot.blob!)
a.download = `snapshot-${shot.wallClockMs}.png`;
a.click();

A common pattern is to store onSnapshot's dataUrl in state and render a thumbnail (<img src={dataUrl} />). For lower-level use, snapshotCanvasElement / snapshotVideoElement are exported too.

Save Clip

The clip flow is drag-to-select on the timeline, then export:

  1. **✂ Create clip** in the bar enters clip mode — it seeds a selection at the playhead and zooms the timeline so it's easy to adjust.
  2. On the timeline you get a shaded region with draggable start/end handles, a draggable body (move the whole range), and a live duration label. The selection clamps to a minimum (default 5s), a maximum (default 60 min — the server cap), and never includes the future.
  3. **Save clip** opens a small title / description / visibility form (skippable — set saveClip.showOptionsForm: false), then runs the export: /video/spliceV3 → progress polling → a download URL.

onClipRangeSelect({ startMs, endMs, cameraUuid }) fires as the selection changes, and onClipExport(status) reports progress/result.

Proxy mode required for export. The clip endpoints are API-key / session authed, not federated-token compatible, so the request must go through your backend (which attaches the API key) — exactly like the media-URI proxy. Built-in export is only available when apiOverrideBaseUrl is set; selection + onClipRangeSelect work regardless. See the Backend contract.

<RhombusPlayer
  cameraUuid="…"
  apiOverrideBaseUrl="https://your-api.example.com"
  saveClip={{ defaultDurationSec: 30, defaultVisibility: "PRIVATE", showOptionsForm: true }}
  onClipExport={(s) => {
    if (s.phase === "rendering") setProgress(s.percentComplete);
    if (s.phase === "complete") window.location.assign(s.downloadUrl!);
  }}
/>
type RhombusSaveClipConfig = {
  enabled?: boolean;           // default true when apiOverrideBaseUrl is set
  paths?: { splice?: string; progress?: string; download?: string };
  defaultTitle?: string;
  defaultDurationSec?: number; // seeded selection width. Default 60
  minDurationSec?: number;     // drag clamp. Default 5
  maxDurationSec?: number;     // drag clamp. Default 3600 (server caps at 60 min)
  progressTimeoutMs?: number;  // give up polling a stuck render. Default 300000 (5 min); 0 = never
  defaultVisibility?: RhombusClipVisibility; // "ORG_WIDE" (default) | "PRIVATE" | "ROLE_RESTRICTED"
  showOptionsForm?: boolean;   // show the title/description/visibility form. Default true
};

type RhombusClipExportOptions = {
  title?: string;
  description?: string;
  visibility?: RhombusClipVisibility;
  saveToConsole?: boolean;     // default true
  audioIncluded?: boolean;     // also splices the camera's .a0 audio facet
};

type RhombusClipExportStatus = {
  phase: "selecting" | "submitting" | "rendering" | "complete" | "error" | "canceled";
  clipUuid?: string;
  percentComplete?: number;  // 0–100 while rendering
  currentOperation?: string;
  downloadUrl?: string;      // set when complete
  error?: string;
};

Build your own clip UI instead of the built-in form: read the live selection from onClipRangeSelect (or getState().clipSelection) and call the imperative handle with your own options:

await player.current!.startClipExport(
  { startMs, endMs, cameraUuid },
  { title: "Front door", visibility: "PRIVATE", audioIncluded: true }
);

Timeline configuration

RhombusPlayer renders a Timeline when controls includes "timeline" (the default). Configure it with the timeline prop:

type RhombusPlayerTimelineConfig = {
  windowSec?: number;        // span of the scrubber, seconds. Default 86400 (a full day)
  fetchSeekPoints?: boolean; // fetch event markers from /camera/getFootageSeekpointsV2. Default true
  includeAnyMotion?: boolean;
  marks?: TimelineMark[];    // extra static event bands / gaps
  colors?: TimelineColors;   // recolor seekpoints, bars, playhead, buttons (see below)
  height?: number;           // px, default 56
  onSeekPointsLoaded?: (points: RhombusFootageSeekPoint[]) => void; // diagnostics
};

By default the window is a 24h span aligned to local midnight (Console-style). RhombusPlayer renders ‹/› chevrons that pan by half a span (±12h at the day view), and −/+ zoom buttons + mouse-wheel zoom that step through 24h → 8h → 3h → 1h → 20m → 5m (centered on the cursor or playhead, with an animated transition) so you can pinpoint a moment when seekpoints bunch up. It auto-follows the current day/playhead until you navigate; Go Live resets to the day view.

The player keeps the window stable while you scrub and only scrolls it once playback leaves the visible range, so the playhead always lands exactly where you click.

RhombusPlayer recipes

Open straight into an event (past footage):

<RhombusPlayer
  cameraUuid="…"
  apiOverrideBaseUrl="https://api.example.com"
  initialMode="vod"
  initialStartTimeMs={new Date("2025-04-15T09:30:00Z").getTime()}
/>

Auto-return to live when caught up, wider rewind step:

<RhombusPlayer cameraUuid="…" autoGoLiveAtEdge defaultRewindSec={30} />

Force broadest browser support (buffered live everywhere):

<RhombusPlayer cameraUuid="…" liveTransport="buffered" />

Headless — your UI, our engine:

function MyPlayer() {
  const ref = useRef<RhombusPlayerHandle>(null);
  const [state, setState] = useState<RhombusPlayerState>();
  return (
    <>
      <RhombusPlayer ref={ref} cameraUuid="…" controls={[]} onModeChange={() => setState(ref.current?.getState())} />
      {/* render your own toolbar from `state` and `ref.current` */}
    </>
  );
}

RhombusBufferedPlayer — DASH live & VOD

Renders live or historical footage with Dash.js into a <video> element. This is the right choice when you want native <video> semantics, the widest browser support, or you're composing your own layout.

<RhombusBufferedPlayer
  cameraUuid="YOUR_CAMERA_UUID"
  connectionMode="wan"          // "wan" (default) | "lan"
  bufferedStreamQuality="HIGH"  // "HIGH" | "MEDIUM" | "LOW"
  videoProps={{ controls: true, style: { width: "100%" } }}
  onReady={() => console.log("playing")}
  onError={(e) => console.error(e)}
/>

Shared base props (all players)

These come from RhombusPlayerBaseProps and are accepted by every player:

| Prop | Type | Default | Notes | | ----------------------- | ----------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | cameraUuid | string | — (required) | Camera UUID from Rhombus. Safe in the browser. | | connectionMode | "wan" | "lan" | "wan"¹ | Which getMediaUris URIs to use. See WAN vs LAN. | | apiOverrideBaseUrl | string | — | Base for the token and media requests. Set for proxy mode. When omitted, media is fetched directly from Rhombus (needs a domain-scoped token). | | rhombusApiBaseUrl | string | https://api2.rhombussystems.com/api | Rhombus REST base when apiOverrideBaseUrl is omitted. | | paths | { federatedToken?, mediaUris?, footageSeekpoints? } | see backend | Override route paths. | | federatedSessionToken | string | — | Supply & rotate your own token; the SDK skips its token endpoint. | | tokenDurationSec | number | 86400 | Requested token TTL (SDK-managed mode). | | headers | HeadersInit | — | Static headers for the token request (+ media when apiOverrideBaseUrl set). | | getRequestHeaders | () => HeadersInit | Promise<…> | — | Async headers merged after headers. | | maxRetryIntervalMs | number | 30000 | Auto-recovery backoff ceiling. 0 disables. | | stallTimeoutMs | number | 12000 | Stall watchdog. 0 disables. | | onRecoveryAttempt | (attempt, error) => void | — | Fires on each retry. | | className / style | string / CSSProperties | — | Applied to the player element. | | onError | (error: Error) => void | — | Token / media / setup failure. |

¹ connectionMode is required (no default) on RhombusRealtimePlayer.

RhombusBufferedPlayer-specific props

| Prop | Type | Default | Notes | | ---------------------------- | --------------------------- | -------- | -------------------------------------------------------------------------------------- | | startTimeSec | number (Unix seconds) | — | Set to play the past (VOD). Omit for live. Changing it re-attaches a new manifest. | | vodDurationSec | number | 7200 | VOD window length; how far you can seek before a new manifest is needed. | | seekOffsetSec | number | 0 | Where in the window playback begins. | | bufferedStreamQuality | "HIGH" | "MEDIUM" | "LOW" | "HIGH" | Server downscale via _ds. Updating doesn't re-fetch the manifest. | | applyBufferedStreamQuality | boolean | true | false omits _ds (full resolution). | | videoProps | VideoHTMLAttributes | — | Spread onto the <video> (controls, muted, onClick, style, …). | | onReady | () => void | — | Dash.js initialized and manifest loaded. |

Exposes a ref handle: { getVideoElement(), getDashPlayer() }.

Live vs. past — the single switch is startTimeSec:

function CameraPlayer({ cameraUuid, mode }: { cameraUuid: string; mode: "live" | "past" }) {
  const startTimeSec =
    mode === "past" ? Math.floor(new Date("2025-04-15T00:00:00Z").getTime() / 1000) : undefined;
  return (
    <RhombusBufferedPlayer
      cameraUuid={cameraUuid}
      startTimeSec={startTimeSec}  // undefined => live, number => VOD
      vodDurationSec={3600}
      videoProps={{ controls: true }}
    />
  );
}

Scrub beyond the window by updating startTimeSec from your own timeline:

const [startTimeSec, setStartTimeSec] = useState(() => Math.floor(Date.now() / 1000) - 3600);
<input type="datetime-local" onChange={(e) => {
  const ms = new Date(e.target.value).getTime();
  if (!Number.isNaN(ms)) setStartTimeSec(Math.floor(ms / 1000)); // loads a fresh window
}} />
<RhombusBufferedPlayer cameraUuid={cameraUuid} startTimeSec={startTimeSec} videoProps={{ controls: true }} />

Pausing live DASH lets it fall behind the live edge; Dash.js catches up on resume. For frame-accurate pause use VOD mode (startTimeSec) — or just use RhombusPlayer, which handles this for you.

formatVodMpdUri(template, startTimeSec, durationSec) and getDefaultRhombusVodDashSettings() are exported if you need to build VOD URLs or tune Dash.js yourself.


RhombusRealtimePlayer — low-latency live

Live H.264 over WebSocket, decoded with WebCodecs onto a <canvas>. Live only — no pause, seek, or VOD. Sub-second latency; ideal for a video wall or PTZ control.

<RhombusRealtimePlayer
  cameraUuid="YOUR_CAMERA_UUID"
  connectionMode="wan"          // REQUIRED: "wan" | "lan"
  realtimeStreamQuality="HD"    // "HD" (/ws) | "SD" (/wsl)
  style={{ width: "100%", background: "#111" }}
  onReady={() => console.log("connected")}
  onError={(e) => console.error(e)}
/>

Accepts all shared base props, plus:

| Prop | Type | Default | Notes | | ----------------------- | ---------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------- | | connectionMode | "wan" | "lan" | — (required) | wanwanLiveH264Uri(s); lanlanLiveH264Uri(s). Federated auth is added as query params on the socket URL. | | realtimeStreamQuality | "HD" | "SD" | "HD" | SD rewrites /ws/wsl. Changing it reconnects (brief blip). | | canvasProps | CanvasHTMLAttributes | — | Spread onto the <canvas>. | | onReady | () => void | — | Fires on every WebSocket OPEN (first connect and each reconnect). |

Exposes a ref handle: { getCanvasElement() }.

**onReady and token rotation differ from buffered:** realtime onReady fires on every (re)connect, and because auth is on the socket URL, each token refresh closes/reopens the socket (short blip). The buffered player rotates tokens without a teardown.

Optional low-level exports for custom wiring: resolveLiveH264WebSocketUrl(options), startRhombusRealtimeSession(options).


Timeline — standalone scrubber

A vendor-neutral canvas scrubber. It does not embed a player — pair it with any video source (or let RhombusPlayer drive it for you). It draws an availability bar, event seekpoints (optionally fetched from /camera/getFootageSeekpointsV2), static marks, a playhead, and a hover line, and emits onSeek(wallClockMs) on click/drag.

import { Timeline } from "@rhombussystems/react";

<Timeline
  cameraUuid="YOUR_CAMERA_UUID"
  apiOverrideBaseUrl="https://your-api.example.com"
  rangeStartMs={Date.now() - 3_600_000}
  rangeEndMs={Date.now()}
  currentTimeMs={playheadMs}
  fetchSeekPoints
  marks={[{ startMs: t0, endMs: t1, kind: "event", color: "#f80", label: "Motion" }]}
  onSeek={(ms) => setPlayheadMs(ms)}
  onHoverTimeChange={(ms) => setHoverMs(ms)}
/>

Accepts the shared base props (for the seekpoint fetch) plus:

| Prop | Type | Default | Notes | | --------------------------------------------------- | ------------------------------------------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------- | | rangeStartMs / rangeEndMs | number (epoch ms) | — (required) | Visible time window. | | currentTimeMs | number | null | — | Playhead position; omit to hide it. | | onSeek | (wallClockMs) => void | — (required) | Click/drag to seek. | | onHoverTimeChange | (wallClockMs | null) => void | — | Pointer hover time. | | selection | { startMs, endMs } | null | — | Clip selection. When set, draws a shaded region + draggable start/end handles + body + duration label. | | onSelectionChange | ({ startMs, endMs }) => void | — | Fired as the user drags the selection. | | selectionMinDurationMs / selectionMaxDurationMs | number | 5000 / 3600000 | Drag clamps for the selection. | | onShiftWindow | (direction: -1 | 1) => void | — | When provided, renders ‹/› chevrons that pan the window (-1 earlier, 1 later). | | canShiftBack / canShiftForward | boolean | true | Enable/disable the respective chevron at a limit. | | onZoom | (zoomIn: boolean, centerWallClockMs: number) => void | — | When provided, enables −/+ zoom buttons and mouse-wheel zoom (centered on the cursor). Range changes animate. | | canZoomIn / canZoomOut | boolean | true | Enable/disable the respective zoom button at a limit. | | fetchSeekPoints | boolean | false | Fetch event markers for the range. Rendered as clustered colored dashes grouped by activity type. | | includeAnyMotion | boolean | true | Include generic motion in the fetch. | | marks | TimelineMark[] | — | Static event bands (kind:"event") / gaps (kind:"gap"). | | onSeekPointsLoaded | (RhombusFootageSeekPoint[]) => void | — | Normalized seekpoints after each fetch (handy for diagnostics). | | colors | TimelineColors | — | Override the canvas-drawn colors (see Theming the timeline). | | height | number | 56 | Canvas height in px. |

Timeline also draws a time axis with auto-spaced tick labels (interval chosen for ~6 divisions, h a / h:mm a format), an availability bar, a playhead, and a hover line.

Exposes a ref handle: { refresh() } to force a seekpoint refetch.

Theming the timeline

The timeline is drawn on a <canvas>, so its colors can't be set with CSS. Pass a colors object instead (every field optional, merged over the defaults). On RhombusPlayer use timeline={{ colors: … }}; on the standalone Timeline use the colors prop:

<RhombusPlayer
  cameraUuid="…"
  timeline={{
    colors: {
      background: "#0b1220",          // canvas fill (default transparent)
      availabilityActive: "#22c55e",  // recorded-footage bar
      availabilityInactive: "#334155",// empty/future bar
      playhead: "#f59e0b",
      hover: "rgba(255,255,255,0.6)",
      tick: "#475569",
      tickLabel: "#94a3b8",
      seekpointDefault: "#60a5fa",     // activities not in eventColors
      seekpointAlert: "#ef4444",       // alerted events
      eventColors: {                   // merged over the built-in per-activity palette
        MOTION_HUMAN: "#facc15",
        MOTION_CAR: "#38bdf8",
        FACE: "#34d399",
      },
      buttonBackground: "#1e293b",     // ‹/›/−/+ buttons
      buttonBorder: "#475569",
      buttonText: "#e2e8f0",
      selection: "rgba(59,130,246,0.22)", // clip-selection region
      selectionHandle: "#3b82f6",         // clip-selection drag handles
    },
  }}
/>

eventColors keys are activity strings from getFootageSeekpointsV2 (e.g. MOTION, MOTION_HUMAN, MOTION_CAR, MOTION_ANIMAL, FACE, SOUND_LOUD, …). The timeline's wrapper (and RhombusPlayer's root) can still be styled via className/style / classNames.timelinecolors.background paints the canvas itself.

Pairing it with a video source

Timeline is just a seek UI — it has no idea what's playing. You wire it to a video by (a) feeding it the current playhead as currentTimeMs, and (b) handling onSeek to move that video. Here it is paired with a RhombusBufferedPlayer in VOD mode, using the player's ref handle (getVideoElement()) to read and drive the underlying <video>. Wall-clock maps to the video as windowStart + video.currentTime:

import { useEffect, useRef, useState } from "react";
import {
  RhombusBufferedPlayer,
  Timeline,
  type RhombusBufferedPlayerHandle,
} from "@rhombussystems/react";

function ScrubbableVod({ cameraUuid }: { cameraUuid: string }) {
  const player = useRef<RhombusBufferedPlayerHandle>(null);
  const windowSec = 3600;
  // Epoch seconds of the VOD manifest window the player currently has loaded.
  const [windowStartSec, setWindowStartSec] = useState(() => Math.floor(Date.now() / 1000) - windowSec);
  const [currentMs, setCurrentMs] = useState(windowStartSec * 1000);

  // Drive the playhead from the <video>'s position.
  useEffect(() => {
    const id = setInterval(() => {
      const v = player.current?.getVideoElement();
      if (v) setCurrentMs(windowStartSec * 1000 + v.currentTime * 1000);
    }, 250);
    return () => clearInterval(id);
  }, [windowStartSec]);

  function handleSeek(ms: number) {
    const v = player.current?.getVideoElement();
    const offsetSec = (ms - windowStartSec * 1000) / 1000;
    if (v && offsetSec >= 0 && offsetSec <= windowSec) {
      v.currentTime = offsetSec;                  // inside the loaded window — instant
    } else {
      setWindowStartSec(Math.floor(ms / 1000));   // outside — load a fresh window at that time
    }
    setCurrentMs(ms);
  }

  return (
    <>
      <RhombusBufferedPlayer
        ref={player}
        cameraUuid={cameraUuid}
        apiOverrideBaseUrl="https://your-api.example.com"
        startTimeSec={windowStartSec}
        vodDurationSec={windowSec}
        videoProps={{ controls: false }}
      />
      <Timeline
        cameraUuid={cameraUuid}
        apiOverrideBaseUrl="https://your-api.example.com"
        rangeStartMs={windowStartSec * 1000}
        rangeEndMs={windowStartSec * 1000 + windowSec * 1000}
        currentTimeMs={currentMs}
        fetchSeekPoints
        onSeek={handleSeek}
      />
    </>
  );
}

The same two wires work for any video: a plain <video> (read/set video.currentTime), an HLS/DASH player, or a multi-camera wall sharing one playhead. (RhombusPlayer does exactly this internally — reach for it if you don't want to own the wiring yourself.)


Authentication & tokens

The SDK is built around short-lived federated session tokens minted by your backend; your Rhombus API key must never reach the browser.

SDK-managed (recommended)

Omit federatedSessionToken. The SDK POSTs to your token route (default /api/federated-token) with { "durationSec": <tokenDurationSec> } and auto-refreshes before expiry (~97% of the effective TTL). Effective TTL = min of your tokenDurationSec and any server hint in the response (expiresInSec, expiresAtMs, or expiresAt).

  • DASH / buffered: keeps playing across refreshes (requests read the latest token).
  • Realtime: reconnects the socket on each refresh (short blip).

You-managed

Pass federatedSessionToken. The SDK never calls your token endpoint. Rotate by passing a new string — DASH picks it up without a teardown; realtime reconnects.

Two transport topologies

| | apiOverrideBaseUrl omitted | apiOverrideBaseUrl set (proxy mode) | | ----------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | Token request | window.location.origin + paths.federatedToken | apiOverrideBaseUrl + paths.federatedToken | | Media-URI request | Direct to Rhombus api2.rhombussystems.com | apiOverrideBaseUrl + paths.mediaUris | | Requirement | Token minted with a Rhombus **domain** allowing this origin, or the browser call is blocked (CORS / 401) | Your backend proxies getMediaUris; browser never talks to Rhombus directly |

Proxy mode is also required for built-in Save Clip (see Save Clip).


WAN vs LAN

connectionMode selects