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

Published

React components for Rhombus video, audio playback, and two-way talkback

Readme

Rhombus React SDK — @rhombussystems/react

React + TypeScript components for embedding Rhombus video and audio in your own app. The SDK supports MPEG-DASH video, low-latency H.264, live and historical A100/DR40 audio, synchronized wall-clock controls, and browser-microphone talkback.

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.2.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 and the worker-backed Opus decoder are bundled — you do not install them separately.
  • The realtime/canvas path uses the browser WebCodecs VideoDecoder (Chrome, Edge, Safari 16.4+; Firefox H.264 is still limited) — no extra dependency.
  • This is a browser media package. In an SSR framework, load the SDK from a client-only module; Dash.js evaluates browser globals when the package is imported.

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, proxy-mode audio, and talkback capability policy additionally need application-owned 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).
  • RhombusAudioPlayer — A100/DR40 live Opus and historical audio, standalone or synchronized with video.
  • RhombusTalkback — send the browser microphone to an A100 or DR40, with Console-aligned RBAC/license/config policy.

Audio full-stack quick start

This is the recommended starting point when a page needs the complete audio feature set:

  • live and historical A100 or DR40 listening;
  • video and audio driven by one epoch-ms timeline;
  • browser-microphone talkback;
  • automatic A100/DR40 click-to-talk versus hold-to-talk behavior;
  • optional talkback blocking while the operator is viewing history;
  • matching-source echo suppression while the operator is speaking; and
  • automatic DR40 ownership handoff so embedded video audio is not played twice.
import {
  RhombusMediaPlayer,
  type RhombusAudioSource,
} from "@rhombussystems/react";

type AudioStationProps = {
  audioSource: RhombusAudioSource;
  /** Omit for an audio-only page. */
  cameraUuid?: string;
};

export function AudioStation({ audioSource, cameraUuid }: AudioStationProps) {
  return (
    <RhombusMediaPlayer
      cameraUuid={cameraUuid}
      audioSource={audioSource}
      apiOverrideBaseUrl="/"
    />
  );
}

Use it with an A100 audio gateway:

<AudioStation
  cameraUuid="CAMERA_UUID"
  audioSource={{ type: "audio-gateway", uuid: "A100_AUDIO_GATEWAY_UUID" }}
/>

Use it with a DR40:

<AudioStation
  cameraUuid="DR40_DEVICE_UUID"
  audioSource={{ type: "dr40", uuid: "DR40_DEVICE_UUID" }}
/>

For a DR40, the video cameraUuid and audioSource.uuid must be the same device UUID for automatic audio ownership handoff — RhombusMediaPlayer then also infers deviceType: "doorbell" for the video participant, so its media resolves through /doorbellcamera/getMediaUris. (When composing RhombusPlayer yourself, pass deviceType="doorbell" explicitly.) An A100 uses its audio gateway UUID, which is normally different from the camera UUID. RhombusMediaPlayer creates and shares the controller automatically: the video timeline seeks both streams, the talkback control knows whether the page is live or historical, and matching incoming far-audio is suppressed while speaking. Use the lower-level composition recipe when your layout needs independent placement of those participants.

Your application backend must expose these routes (defaults shown):

| Browser route | Purpose | Rhombus upstream | | --- | --- | --- | | POST /api/federated-token | Mint a short-lived browser token. | /org/generateFederatedSessionToken | | POST /api/audio-media-uris | Resolve A100 or DR40 live/VOD media URIs. | /audiogateway/getMediaUris or /doorbellcamera/getMediaUris | | POST /api/audio-talkback-capabilities | Normalize device-scope authorization, Enterprise licensing, speaker configuration, connectivity, and interaction mode. | Accessible-device inventory, getConfig, and /license/getDeviceLicenses |

The backend keeps the API key secret. The realtime audio WebSocket separately authenticates the federated token and must authorize its permission group for the requested device; the capability route is a user-interface policy check, not the WebSocket security boundary. Copyable route contracts are in Backend contract.

Before testing, confirm:

  1. The server-side API key can see the selected device through its assigned permission group.
  2. The A100/DR40 has an Enterprise device license, its speaker is enabled, and it is online.
  3. The federated token is minted for the browser's deployed domain.
  4. The page is served over HTTPS (or localhost), microphone access is allowed by the browser, operating system, and iframe policy, and the media/worker hosts are allowed by CSP.
  5. The user explicitly unmutes listening audio and starts talkback from a click, tap, or keyboard gesture so browser media activation succeeds.

Talkback is allowed while viewing historical footage by default. Set disableTalkbackInVod when the operator must return to live before speaking. Talkback always reaches the physical device now; it is never scheduled at the historical playhead.


Choosing a component

| Component | Transport | Live latency | Live | Past (VOD) | Controls | | --------------------------- | --------------------------------------------------------------------- | --------------- | ---- | ---------- | ---------------------- | | RhombusMediaPlayer | unified video + A100/DR40 audio + talkback | sub-second live | ✅ | ✅ | ✅ complete experience | | RhombusPlayer | both — realtime canvas for live, DASH for VOD, switched automatically | sub-second live | ✅ | ✅ | ✅ full bar + ref API | | RhombusAudioPlayer | live Opus + historical DASH/decoded Opus | sub-second live | ✅ | ✅ | ✅ full bar + ref API | | RhombusTalkback | browser microphone → PCM16/WebSocket → A100/DR40 speaker | sub-second | ✅ | n/a | ✅ mic control + 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 media | — | — | — | seek UI only |

Rule of thumb: start with RhombusMediaPlayer for the complete experience. Reach for RhombusPlayer, RhombusAudioPlayer, and RhombusTalkback separately when the application needs custom participant placement or lifecycle. Give separate participants the same shared controller when playback time, VOD talk policy, and two-way-audio echo handling must coordinate.


RhombusMediaPlayer — complete video, audio, and talkback

RhombusMediaPlayer is the high-level facade for the most common integration. It composes the existing video, audio, talkback, and controller implementations; it does not introduce a second transport stack. The required audioSource name is deliberately explicit because cameraUuid identifies a different, optional video participant.

Identity props: why audioSource is required

The two identity props name two different participants:

| Prop | Identifies | When required | | --- | --- | --- | | audioSource | The A100 audio gateway or DR40 used for incoming audio and talkback. | Always | | cameraUuid | The optional camera that supplies video and the shared timeline. | Only for video + audio |

Therefore omitting cameraUuid means audio-only; it does not make audioSource generic. For a DR40 A/V station, use the same DR40 UUID for both props. For an A100 station, audioSource.uuid is the A100 gateway UUID and cameraUuid is the separately chosen camera.

Copy-paste defaults

The minimum audio-only experience is:

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

<RhombusMediaPlayer
  audioSource={{ type: "audio-gateway", uuid: "A100_AUDIO_GATEWAY_UUID" }}
  apiOverrideBaseUrl="/"
/>;

Add cameraUuid for synchronized video:

<RhombusMediaPlayer
  cameraUuid="CAMERA_UUID"
  audioSource={{ type: "audio-gateway", uuid: "A100_AUDIO_GATEWAY_UUID" }}
  apiOverrideBaseUrl="/"
/>;

Those examples intentionally pass no control configuration, participant overrides, custom styles, callbacks, or external controller. Out of the box, the facade supplies:

  • the complete audio toolbar and audio timeline on an audio-only page;
  • the video toolbar and its single shared timeline when cameraUuid is present;
  • only volume control on the separate audio row when video owns the timeline;
  • listening audio, initially muted for browser autoplay compatibility;
  • talkback with the device-resolved click-to-talk or hold-to-talk interaction;
  • live and historical playback with synchronized timeline seeks;
  • automatic matching-DR40 audio ownership handoff; and
  • a private shared playback controller that requires no application wiring.

The sibling rhombus-react-example project exposes /media-player as a dedicated out-of-the-box test page. Its selectors are only test-fixture UI; the rendered RhombusMediaPlayer receives exactly the props shown above. Use its /audio page for the advanced customization and diagnostics lab.

Defaults:

  • talkback is rendered and follows the server-resolved hold/toggle policy;
  • talkback remains available in VOD and always targets the live physical device;
  • one private RhombusPlaybackController coordinates every participant;
  • without video, audio renders its complete controls and wall-clock timeline;
  • with video, the video owns the timeline and audio renders volume controls only;
  • matching DR40 video/audio automatically hands buffered/VOD audio ownership to video;
  • listening starts muted, volume 1, playing, live, and at rate 1; and
  • the facade uses the same token, endpoint, network, recovery, and error props for every participant.

Set talkback={false} for playback only. Set disableTalkbackInVod when operators must return to the live edge before speaking.

RhombusMediaPlayer props

| Prop | Type | Default | Purpose | | --- | --- | --- | --- | | audioSource | RhombusAudioSource | required | A100 audio gateway or DR40 used for listening and talkback. | | cameraUuid | string | — | Optional synchronized video participant. Omit for audio-only. | | deviceType | "camera" | "doorbell" | inferred | Video device family. Defaults to "doorbell" when cameraUuid matches a "dr40" audioSource, else "camera". | | apiOverrideBaseUrl and shared media props | RhombusMediaBaseProps | SDK defaults | Applied consistently to video, audio, and talkback. | | playbackController | RhombusPlaybackController | private controller | Join an existing playback group instead of creating one. | | playbackOptions | RhombusPlaybackControllerOptions | controller defaults | Seed the private controller's mode, time, play state, rate, volume, mute, and timeline behavior. | | talkback | boolean | true | Render or omit microphone talkback. | | disableTalkbackInVod | boolean | false | Block and immediately stop TX while viewing history. | | videoProps | RhombusMediaPlayerVideoProps | — | Video-specific controls, quality, fit, callbacks, and styling. | | audioProps | RhombusMediaPlayerAudioProps | contextual controls | Audio-specific controls, VOD window, callbacks, and styling. | | talkbackProps | RhombusMediaPlayerTalkbackProps | — | Talkback interaction, microphone, capability, callback, and styling overrides. | | className / style | React root styling | — | Customize the facade root. | | classNames / styles | facade slot maps | — | Customize root, video, audio, and talkback slots. Inline values override defaults. | | onError / onRecoveryAttempt | shared callbacks | — | Receive failures/recovery from every participant. |

The nested prop types intentionally omit participant identity, shared authentication, controller-owned playback state, and the shared controller itself. This prevents a nested override from silently splitting the group. Use playbackOptions to seed internal state:

<RhombusMediaPlayer
  cameraUuid={cameraUuid}
  audioSource={audioSource}
  apiOverrideBaseUrl="/"
  disableTalkbackInVod
  playbackOptions={{
    initialMuted: true,
    defaultRewindSec: 30,
  }}
  videoProps={{
    videoFit: "contain",
    timeline: { fetchSeekPoints: true },
  }}
  audioProps={{
    vodWindowSec: 30 * 60,
  }}
  talkbackProps={{
    microphoneGain: 5,
  }}
/>;

When cameraUuid is present, audioProps.controls defaults to ["volume"]. Pass an explicit list to change it. Without a camera, an omitted control list renders the full audio toolbar and timeline.

Styling and imperative access

The facade uses zero-specificity defaults on:

  • .rhombus-media-player
  • .rhombus-media-player-video
  • .rhombus-media-player-audio
  • .rhombus-media-player-talkback

Normal CSS overrides those classes. classNames adds design-system classes, while styles sets inline styles on the corresponding slots. The root also exposes data-rhombus-media-has-video, data-rhombus-media-audio-source, and data-rhombus-media-talkback for state-aware selectors:

<RhombusMediaPlayer
  audioSource={audioSource}
  className="security-station"
  classNames={{ talkback: "security-station-microphone" }}
  styles={{
    root: { gap: 16 },
    talkback: { borderColor: "var(--brand-border)" },
  }}
/>;

The RhombusMediaPlayerHandle exposes the shared playbackController plus getVideoPlayer(), getAudioPlayer(), and getTalkback(). The getters return each existing participant handle or null when that participant is disabled:

import { useRef } from "react";
import {
  RhombusMediaPlayer,
  type RhombusMediaPlayerHandle,
} from "@rhombussystems/react";

const media = useRef<RhombusMediaPlayerHandle>(null);

<RhombusMediaPlayer ref={media} audioSource={audioSource} />;

<button onClick={() => media.current?.playbackController.goLive()}>
  Go live
</button>

Use the lower-level components when participants must render in unrelated parts of the DOM, when separate controllers are intentional, or when application-specific layout exceeds the facade's video → audio → talkback ordering.


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. A seek preserves the play/pause state: if playback was paused, it stays paused at the new time; if playing, it keeps playing (seeking to the live edge always resumes, since realtime live cannot be paused).

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. For a DR40 this is the doorbell's device UUID. | | deviceType | "camera" | "doorbell" | — | "camera" | Set "doorbell" for DR40 video intercoms so media/seekpoint/availability requests target the /doorbellcamera/* endpoints (direct mode) or tag proxy bodies for routing. | | 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 | RhombusPlayerPaths | — | see backend | Override video, audio, token, seekpoint, and availability routes. | | 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. | | playbackController | RhombusPlaybackController | — | private controller | Join one video and one audio participant. Controller playhead/rate/mute/volume state takes precedence over equivalent player props. | | 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" | "goToDate"
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={[]} />
</>

Go to date ("goToDate" control / RhombusDateTimePicker)

The toolbar includes a date/time jump picker (the "goToDate" control, on by default): a calendar + time-of-day popover that seeks the player to any moment — the SDK counterpart of the Rhombus Console's toolbar date picker. When footage availability is enabled, days with no recorded footage are struck through and disabled (one getPresenceWindows fetch per viewed month, cached; failures just leave days enabled).

The component is also exported standalone for custom layouts — it interops with any player via seekTo:

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

<RhombusDateTimePicker
  value={positionMs}                              // epoch ms (or null)
  onChange={(ms) => playerRef.current?.seekTo(ms)}
  cameraUuid="…"                                  // optional: enables no-footage day disabling
  apiOverrideBaseUrl="https://your-api.example.com"
  minTimeMs={retentionFloorMs}                    // optional: disable pre-retention days
  direction="down"                                // "up" for bottom toolbars (player default)
/>

All calendar math is in the viewer's local time zone (consistent with the Timeline). Style it via the rhombus-datepicker-* classes (zero-specificity defaults, like the control bar) or the classNames={{ anchor, popover }} prop.

Inside RhombusPlayer, a picker jump that lands outside the visible timeline window also re-centers the timeline on the target (in-window seeks — i.e. timeline clicks — never move the window). In custom layouts composing the standalone picker with a standalone Timeline, you own the window: update your rangeStartMs/rangeEndMs in the same onChange that calls seekTo.

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
  requireFootage?: "any" | "full" | "off"; // footage pre-check policy. Default "any" (see below)
};

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;
  errorCode?: "no-footage" | "partial-footage"; // set when the footage pre-check blocked the export
  coverage?: RhombusRangeCoverage;              // footage coverage of the range, when the check ran
};

Footage pre-check (requireFootage)

Rhombus renders time ranges with no recorded footage (camera offline during the window, or footage past retention) as "VIDEO NOT AVAILABLE" placeholder frames — and /video/spliceV3 happily renders a clip over such a range, returning a "successful" clip with no real video in it. Before submitting an export, the player therefore fetches /camera/getPresenceWindows for the selected range and applies requireFootage:

  • "any" (default) — block only when the range has zero recorded footage.
  • "full" — block when the range has any confirmed gap.
  • "off" — no pre-check (legacy behavior).

A blocked export emits phase: "error" with errorCode ("no-footage" / "partial-footage") and coverage — key both your custom UI messages off errorCode, not the human-readable error string. Exports that proceed carry coverage on every subsequent status so you can warn about partial footage. The check fails open: if availability can't be fetched (missing proxy route, timeout), the export proceeds ungated and coverage is absent.

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;
  fetchAvailability?: boolean; // fetch footage coverage from /camera/getPresenceWindows and draw
                               // no-footage gaps on the availability bar. Default: true in proxy
                               // mode (apiOverrideBaseUrl set), false in direct mode.
  onAvailabilityLoaded?: (availability: RhombusFootageAvailability) => void;
  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
};

Footage availability

With fetchAvailability on, the availability bar stops pretending all past time is recorded: ranges with confirmed no footage render in colors.availabilityGap (default a muted red) — the same ranges the Rhombus stream would play as "VIDEO NOT AVAILABLE" placeholder frames. Gaps are only drawn where the answer is actually known (inside the fetched range, in the past, and older than a ~2-minute live-edge grace window for presence-ingest lag); everything else keeps the legacy look. The in-clip-mode toolbar also shows a warning (and disables Save, per requireFootage) when the selection overlaps a gap.

The raw client + coverage math are exported for custom UIs: fetchPresenceWindows, mergeFootageWindows, computeFootageGaps, computeRangeCoverage, and the RhombusFootageWindow / RhombusFootageAvailability / RhombusRangeCoverage types. Windows carry source: "cloud" | "local"local windows live on the camera's SD card and are only retrievable while the camera is online; cloud windows are always retrievable.

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` */}
    </>
  );
}

RhombusAudioPlayer — A100 and DR40 audio

RhombusAudioPlayer is the drop-in audio equivalent of RhombusPlayer. It plays an A100 audio gateway or DR40 by absolute wall-clock time, switches between live and historical audio, and can either own its controls or participate in the same controller and timeline as video.

Use it for:

  • standalone live A100 or DR40 listening;
  • standalone historical audio with rewind, pause, speed, and epoch-ms seeking;
  • synchronized camera video plus independent A100 audio;
  • synchronized DR40 video/audio without downloading or playing the DR40 audio twice;
  • a headless audio engine controlled by application UI;
  • one external timeline controlling both a video and audio participant.

Use RhombusTalkback alongside this component for full two-way audio. DSP/equalizer controls and multiple simultaneous audible tracks on one shared controller remain outside the public API.

Audio sources and device UUIDs

Sources are explicit so the SDK can select the correct Rhombus endpoint and request body:

import type { RhombusAudioSource } from "@rhombussystems/react";

const a100: RhombusAudioSource = {
  type: "audio-gateway",
  uuid: "A100_AUDIO_GATEWAY_UUID",
};

const dr40: RhombusAudioSource = {
  type: "dr40",
  uuid: "DR40_DEVICE_UUID",
};
  • "audio-gateway" calls /audiogateway/getMediaUris with { gatewayUuid: source.uuid }.
  • "dr40" calls /doorbellcamera/getMediaUris with { deviceUuid: source.uuid }.
  • An A100 UUID is the audio gateway UUID, not an associated camera UUID.
  • A source must be visible to the organization represented by the API key or federated token. An unknown or cross-organization UUID produces no usable media URIs.

The SDK intentionally does not include a device-list UI. Most applications select devices from their own inventory. A backend-powered picker can use Rhombus /audiogateway/getMinimalAudioGatewayStateList for A100s and /doorbellcamera/getMinimalStateList for DR40s; keep the API key server-side.

Standalone live audio

The smallest complete player is:

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

export function LobbyAudio() {
  return (
    <RhombusAudioPlayer
      source={{ type: "audio-gateway", uuid: "A100_UUID" }}