Skip to content

TypeScript SDK

This is the complete LLM-context reference for the Adamo TypeScript/React SDK. An LLM given this page should be able to generate a fully working robot teleoperation UI without reading any source code.

The SDK is split across four packages:

PackagePurpose
@adamo/reactReact hooks and components. This is the primary package for building UIs.
@adamo/mediaLow-level H.264 video decoding, FEC recovery, TWCC congestion control, XR stereo rendering. Used internally by @adamo/react.
@adamo/teleopGamepad (joystick) input management and Joy message encoding.
@adamo/fleetImperative (non-React) topic discovery and subscription utilities.

Transport is over WebSocket (wss://) or WebTransport. The SDK resolves the right relay for your organization from the Adamo API at connect time — you never hardcode a relay URL.

Terminal window
npm install @adamo/react @adamo/media @adamo/teleop

Wrap your entire app in <AdamoProvider>. It creates a session and provides it via React context to all child components. It also manages an internal StreamManager that ref-counts active video streams so they survive page navigation without reconnecting.

import { AdamoProvider } from "@adamo/react";
// Connect with an API key (recommended) — the relay URL, org, and
// auth are all resolved from the API automatically.
function App() {
return (
<AdamoProvider apiKey="ak_your_key_here">
<RobotUI />
</AdamoProvider>
);
}

For user-authenticated apps (e.g. Supabase login), pass the user’s JWT as authToken — it is attached to the connection for relays that enforce per-user auth, and refreshed on reconnect:

<AdamoProvider apiKey="ak_your_key_here" authToken={supabaseSession.access_token}>

You can also pass a pre-existing session (useful in Tauri where the session lives in native code):

import { AdamoProvider } from "@adamo/react";
import type { Session } from "@adamo/react";
function App({ session }: { session: Session }) {
return (
<AdamoProvider session={session} org="my-org">
<RobotUI />
</AdamoProvider>
);
}
PropTypeDescription
apiKeystring (optional)Adamo API key (ak_...). Resolves the relay URL, org, and auth automatically. The standard way to connect.
apiUrlstring (optional)API base URL override (defaults to production).
authTokenstring (optional)User JWT attached to the connection for relays enforcing per-user auth. Refreshed on reconnect.
sessionSession (optional)Pre-existing session. Takes precedence over everything else.
urlstring (optional)Advanced: explicit WebSocket relay URL override. apiKey resolves this automatically — only set it for custom topologies.
wtUrlstring (optional)Advanced: explicit WebTransport relay URL override.
orgstring (optional)Advanced: org slug override for topic namespacing (apiKey resolves it).
orgIdstring (optional)Advanced: org UUID override for usage admission (apiKey resolves it).
childrenReact.ReactNodeChild components.

Pass apiKey (standard) or session (advanced). The url/org overrides exist for custom topologies and skip the API’s relay selection.

import { useSession, useOrg } from "@adamo/react";
function MyComponent() {
const session = useSession(); // Session | null
const org = useOrg(); // string | null
if (!session) return <p>Connecting...</p>;
return <p>Connected to org: {org}</p>;
}

useSession() returns Session | null. It is null until the WebSocket connection is established.

useOrg() returns string | null — the org slug passed to <AdamoProvider>.

Discovers online robots by subscribing to liveliness tokens at adamo/{org}/*/alive. Returns the list sorted alphabetically.

import { useRobots } from "@adamo/react";
import type { Robot } from "@adamo/react";
function RobotList() {
const robots = useRobots(); // Robot[]
return (
<ul>
{robots.map((robot) => (
<li key={robot.id}>
{robot.id}{robot.isStreaming ? "online" : "offline"}
</li>
))}
</ul>
);
}
type Robot = {
id: string; // Robot identifier (e.g., "arm-01")
isStreaming: boolean; // true when liveliness token is active
};

Discovers video tracks for a specific robot (or all robots if robotId is omitted) by subscribing to liveliness tokens at adamo/{org}/{robotId}/video/*/alive. Returns tracks sorted with "main" first, then alphabetically.

import { useTracks } from "@adamo/react";
import type { Track } from "@adamo/react";
function TrackSelector({ robotId }: { robotId: string }) {
const tracks = useTracks(robotId); // Track[]
return (
<select>
{tracks.map((track) => (
<option key={track.name} value={track.name}>
{track.name} {track.isActive ? "" : "(inactive)"}
</option>
))}
</select>
);
}
type Track = {
name: string; // Track name (e.g., "main", "kinect_rgb")
robotId: string; // Robot this track belongs to
isActive: boolean; // true when liveliness token is active
};
import { useState } from "react";
import { useRobots, useTracks, Stream } from "@adamo/react";
function RobotBrowser() {
const robots = useRobots();
const [selectedRobot, setSelectedRobot] = useState<string | null>(null);
const [selectedTrack, setSelectedTrack] = useState("main");
const tracks = useTracks(selectedRobot ?? undefined);
return (
<div>
<div>
{robots.map((r) => (
<button
key={r.id}
onClick={() => {
setSelectedRobot(r.id);
setSelectedTrack("main");
}}
style={{ fontWeight: selectedRobot === r.id ? "bold" : "normal" }}
>
{r.id}
</button>
))}
</div>
{selectedRobot && (
<div>
<select
value={selectedTrack}
onChange={(e) => setSelectedTrack(e.target.value)}
>
{tracks.map((t) => (
<option key={t.name} value={t.name}>
{t.name}
</option>
))}
</select>
<Stream
robot={selectedRobot}
track={selectedTrack}
style={{ width: "100%", aspectRatio: "16/9" }}
/>
</div>
)}
</div>
);
}

The primary video component. Subscribes to an H.264 stream, decodes using the WebCodecs API, and renders frames to a canvas. Streams are ref-counted internally so navigating away and back does not cause a reconnect.

The stream automatically sends resize hints to the robot encoder so it only encodes at the resolution the viewer needs.

import { Stream } from "@adamo/react";
// Recommended: identify by robot + track (org comes from AdamoProvider)
<Stream
robot="arm-01"
track="main"
className="w-full h-full"
/>
// Alternative: provide the full topic directly
<Stream
topic="adamo/my-org/arm-01/video/main"
className="w-full h-full"
/>
PropTypeDefaultDescription
robotstringRobot ID. Used with track to build the topic adamo/{org}/{robot}/video/{track}.
trackstring"main"Track name. Used with robot.
topicstringFull key expression for the video topic. Overrides robot/track.
crop{ top, right, bottom, left }Fractional crop insets (0–1) applied via CSS transform. Lets you show a sub-region of the video without re-encoding.
onStats(stats: StreamStats) => voidCalled every second with stream health metrics.
classNamestringCSS class applied to the outer <div>.
styleReact.CSSPropertiesInline styles for the outer <div>. The component sets overflow: hidden internally.

Either topic or robot (with optional track) must be provided.

type StreamStats = {
fps: number; // Decoded frames per second
kbps: number; // Received bitrate in kilobits per second
jitterMs: number; // Inter-packet transit-time jitter (EWMA, ms)
rttMs: number; // Round-trip time in milliseconds (from ping/pong)
queueMs: number; // Time a packet waits in the decode buffer (ms)
decodeMs: number; // Time to decode the last frame (ms)
renderMs: number; // Time to render the last frame (ms)
totalPackets: number; // Total video packets received
fecRecovered: number; // Packets recovered via FEC (cumulative)
// Datagram-ARQ chunk path (optional — present when the stream
// uses chunked datagram transport):
chunksRlncRecovered?: number; // Chunk-level FEC recoveries; subset of fecRecovered
chunksRetransmitted?: number; // Chunks replayed by router-side ARQ
};
// Show only the left half of the video (e.g., for a side-by-side stereo camera)
<Stream
robot="arm-01"
track="stereo"
crop={{ top: 0, right: 0.5, bottom: 0, left: 0 }}
className="w-full h-full"
/>
import { useState } from "react";
import { Stream } from "@adamo/react";
import type { StreamStats } from "@adamo/react";
function StreamWithStats({ robotId }: { robotId: string }) {
const [stats, setStats] = useState<StreamStats | null>(null);
return (
<div style={{ position: "relative" }}>
<Stream robot={robotId} track="main" onStats={setStats} style={{ width: "100%", aspectRatio: "16/9" }} />
{stats && (
<div style={{ position: "absolute", top: 8, left: 8, color: "white", fontSize: 12 }}>
{stats.fps.toFixed(1)} fps | {stats.kbps.toFixed(0)} kbps | RTT {stats.rttMs.toFixed(0)} ms
</div>
)}
</div>
);
}

useSubscription(keyexpr, onSample, options?)

Section titled “useSubscription(keyexpr, onSample, options?)”

Subscribes to a key expression and calls a callback for each received Sample. Uses a RingChannel internally — old messages are dropped when the buffer fills, which is the correct behaviour for real-time streams.

import { useSubscription, json } from "@adamo/react";
function SystemStats({ robotId, org }: { robotId: string; org: string }) {
useSubscription(
`adamo/${org}/${robotId}/stats/system`,
(sample) => {
const stats = json.decode<{ cpu: number; memoryMb: number }>(
sample.payload().toBytes()
);
console.log("CPU:", stats.cpu, "Memory:", stats.memoryMb);
}
);
return null;
}
type SubscriptionOptions = {
bufferSize?: number; // Ring buffer size (default: 8). Increase for bursty topics.
enabled?: boolean; // Set false to pause the subscription without unmounting.
};

useSubscriptionMap<T>(pattern, decode, options?)

Section titled “useSubscriptionMap<T>(pattern, decode, options?)”

Subscribes to a wildcard pattern and returns a Map<string, T> from full key expression to the latest decoded value. Useful for tracking state of multiple robots or sensors simultaneously.

import { useSubscriptionMap, json } from "@adamo/react";
function AllRobotHeartbeats({ org }: { org: string }) {
// Map<keyexpr, heartbeat> — one entry per robot
const heartbeats = useSubscriptionMap(
`adamo/${org}/*/heartbeat`,
(bytes) => json.decode<{ activeTracks: string[] }>(bytes)
);
return (
<ul>
{Array.from(heartbeats.entries()).map(([topic, hb]) => (
<li key={topic}>
{topic}: tracks [{hb.activeTracks.join(", ")}]
</li>
))}
</ul>
);
}

Creates a publisher and returns a handle. Also tracks whether any subscribers are currently matching — useful to avoid encoding or sending data when nobody is listening.

import { usePublisher, json } from "@adamo/react";
function JointStatePublisher({ robotId, org }: { robotId: string; org: string }) {
const publisher = usePublisher(`adamo/${org}/${robotId}/control/joint_cmd`);
const sendCommand = async () => {
if (!publisher) return;
await publisher.put(
json.encode({ positions: [0.1, 0.2, 0.3], velocities: [0, 0, 0] })
);
};
return (
<button onClick={sendCommand} disabled={!publisher?.hasSubscribers}>
{publisher?.hasSubscribers ? "Send Command" : "No robot listening"}
</button>
);
}
type PublisherHandle = {
put: (data: Uint8Array) => Promise<void>;
hasSubscribers: boolean; // true when at least one matching subscriber exists
};
type PublisherOptions = {
priority?: Priority; // Default: Priority.REAL_TIME
congestionControl?: CongestionControl; // Default: CongestionControl.DROP
reliability?: Reliability; // Default: Reliability.BEST_EFFORT
express?: boolean; // Default: true (disable batching for lower latency)
};

For real-time teleop control, keep the defaults: drop + best-effort + express. Use CongestionControl.BLOCK and Reliability.RELIABLE only for configuration changes or commands that must arrive exactly once.

Subscribe to log lines published by a robot via robot.log(...) in the Python, Rust, or C SDKs.

import { useRobotLogs } from "@adamo/react";
function RobotLogView({ robotId }: { robotId: string }) {
const logs = useRobotLogs(robotId);
return (
<ul>
{logs.map((l, i) => (
<li key={i}>
[{l.level}] {new Date(l.ts_us / 1000).toISOString()}{l.message}
</li>
))}
</ul>
);
}

Pass robotId = "*" to aggregate log lines across every robot in the org.

type RobotLog = {
ts_us: number; // fabric-clock microseconds
level: string; // "info" | "warn" | "error" | "debug" | ...
message: string;
keyexpr: string; // the key the sample arrived on
};
type UseRobotLogsOptions = {
max?: number; // Default: 500. Older entries drop once buffer exceeds this.
enabled?: boolean; // Default: true. Set false to pause the subscription.
};

Reads from a browser gamepad and publishes Joy messages on the configured topic. Polls every 20ms by default. The component renders nothing — it is purely a side-effect component.

import { GamepadController } from "@adamo/react";
import type { JoyMessage } from "@adamo/react";
function TeleoperationPage({ robotId, org }: { robotId: string; org: string }) {
return (
<div>
<Stream robot={robotId} track="main" style={{ width: "100%", aspectRatio: "16/9" }} />
<GamepadController
topic={`adamo/${org}/${robotId}/control/joy`}
config={{
deviceId: 0, // Which gamepad index to use (default: 0)
deadzone: 0.05, // Axis deadzone threshold (default: 0.05)
pollIntervalMs: 20 // Poll rate in ms; 0 = requestAnimationFrame
}}
onInput={(msg) => {
// msg is a JoyMessage — axes[0]/axes[1] = left stick, axes[2]/axes[3] = right stick
console.log("left stick x:", msg.axes[0], "y:", msg.axes[1]);
}}
onButtonDown={(idx) => console.log("button pressed:", idx)}
onButtonUp={(idx) => console.log("button released:", idx)}
onConnectionChange={(connected) => {
console.log("gamepad", connected ? "connected" : "disconnected");
}}
/>
</div>
);
}
PropTypeDescription
topicstringkey expression to publish Joy messages on.
configJoypadConfigGamepad configuration (see below).
onInput(msg: JoyMessage) => voidCalled every poll cycle when the gamepad has input.
onButtonDown(buttonIndex: number) => voidCalled when a button transitions from 0 to 1.
onButtonUp(buttonIndex: number) => voidCalled when a button transitions from 1 to 0.
onConnectionChange(connected: boolean) => voidCalled when a gamepad connects or disconnects.
type JoypadConfig = {
deviceId?: number; // Gamepad index from navigator.getGamepads() (default: 0)
deviceName?: string; // Match by partial name instead of index
deadzone?: number; // Axis deadzone [0, 1] (default: 0.05)
pollIntervalMs?: number; // Poll interval in ms; 0 = requestAnimationFrame (default: 20)
stickyButtons?: boolean; // Toggle buttons instead of momentary (default: false)
coalesceIntervalMs?: number; // Coalesce rapid inputs (default: 0 = disabled)
autorepeatRate?: number; // Button autorepeat rate in Hz (default: 0 = disabled)
topic?: string; // Internal topic alias (usually set via the top-level prop)
};

The Joy message shape published by the gamepad component:

type JoyMessage = {
header: {
stamp: {
sec: number;
nanosec: number;
};
frame_id: string; // Always "joy"
};
axes: number[]; // 6 elements: [left_x, left_y, right_x, right_y, lt, rt]
buttons: number[]; // 17 elements, W3C Standard Gamepad mapping
};

Standard Gamepad Button Mapping (W3C / Xbox)

Section titled “Standard Gamepad Button Mapping (W3C / Xbox)”
Index | Button Index | Button
0 | A 8 | Back / Select
1 | B 9 | Start
2 | X 10 | L3 (left stick click)
3 | Y 11 | R3 (right stick click)
4 | LB 12 | D-Pad Up
5 | RB 13 | D-Pad Down
6 | LT 14 | D-Pad Left
7 | RT 15 | D-Pad Right
16 | Guide / Xbox button
Index | Axis
0 | Left stick X (-1 = left, +1 = right)
1 | Left stick Y (-1 = up, +1 = down)
2 | Right stick X (-1 = left, +1 = right)
3 | Right stick Y (-1 = up, +1 = down)
4 | Left trigger (0 = released, 1 = fully pressed)
5 | Right trigger (0 = released, 1 = fully pressed)

Returns a function to send track start/stop requests to a robot. The request is published to adamo/{org}/{robotId}/tracks/request using a reliable publisher.

import { useTrackRequest } from "@adamo/react";
function TrackControls({ robotId }: { robotId: string }) {
const trackRequest = useTrackRequest(robotId);
const startKinect = () =>
trackRequest({
action: "start",
name: "kinect_rgb",
source_type: "v4l2",
bitrate: 2000, // kbps
fps: 30,
});
const startArmCamera = () =>
trackRequest({
action: "start",
name: "arm_camera",
source_type: "v4l2",
v4l2_device: "/dev/video2",
bitrate: 4000,
fps: 60,
});
const stopKinect = () =>
trackRequest({ action: "stop", name: "kinect_rgb" });
return (
<div>
<button onClick={startKinect}>Start Kinect</button>
<button onClick={startArmCamera}>Start Arm Camera</button>
<button onClick={stopKinect}>Stop Kinect</button>
</div>
);
}
type TrackRequestParams = {
action: "start" | "stop";
name: string; // Track name (used as the key expression segment)
// Source configuration (for "start")
source_type?: string; // "v4l2" or "shm"
shm_service?: string; // iceoryx2 service name (when source_type is "shm")
v4l2_device?: string; // V4L2 device path (e.g., "/dev/video0")
v4l2_capture_resolution?: [number, number]; // [width, height] for V4L2 capture
// Encoding
encoder?: string; // Encoder name (e.g., "nvenc", "x264")
bitrate?: number; // Target bitrate in kbps
fps?: number; // Target frame rate
passthrough?: boolean; // Pass compressed data through without re-encoding
source_format?: string; // Input pixel format hint
keyframe_distance?: number; // Keyframe interval in frames
// Stereo
stereo?: boolean; // Top/bottom stereo encoding
// FEC
fec?: boolean; // Enable Reed-Solomon FEC
fec_data_shards?: number; // Data shards (default: 4)
fec_parity_shards?: number; // Parity shards (default: 2)
};

Subscribes to adamo/{org}/{robotId}/video/*/meta and returns a Map<string, TrackMeta> from track name to metadata. Also performs an initial get() to retrieve metadata for already-active tracks.

import { useTrackMeta } from "@adamo/react";
function TrackInfo({ robotId }: { robotId: string }) {
const meta = useTrackMeta(robotId); // Map<string, TrackMeta>
return (
<ul>
{Array.from(meta.entries()).map(([trackName, m]) => (
<li key={trackName}>
{trackName}: {m.stereo ? "stereo" : "mono"}
</li>
))}
</ul>
);
}
type TrackMeta = {
stereo: boolean; // true if the track is top/bottom stereo encoded
};

Subscribes to adamo/{org}/{robotId}/tracks/status and returns a Map<string, TrackStatusEvent> with the latest lifecycle event for each track.

import { useTrackStatus } from "@adamo/react";
function TrackStatusPanel({ robotId }: { robotId: string }) {
const statuses = useTrackStatus(robotId); // Map<string, TrackStatusEvent>
return (
<ul>
{Array.from(statuses.entries()).map(([trackName, event]) => (
<li key={trackName} style={{ color: event.status === "error" ? "red" : "inherit" }}>
{trackName}: {event.status}
{event.error && ` — ${event.error}`}
</li>
))}
</ul>
);
}
type TrackStatusEvent = {
track: string;
status: "starting" | "running" | "error" | "stopped";
error?: string; // Error message when status is "error"
timestamp: number; // Unix timestamp in milliseconds
};

Watches for active topics matching a key expression pattern. A topic is considered active as long as it has published within the TTL window. Returns a sorted string array of currently active topic key expressions.

import { useTopics } from "@adamo/react";
function ActiveVideoStreams({ org }: { org: string }) {
// Discover all active video topics across all robots
const topics = useTopics(`adamo/${org}/*/video/*`, 5000);
return (
<ul>
{topics.map((t) => <li key={t}>{t}</li>)}
</ul>
);
}

<TopicPresenceProvider> and useTopicPresence()

Section titled “<TopicPresenceProvider> and useTopicPresence()”

Provides shared topic presence detection across many child components using a single subscription, avoiding redundant subscriptions when many <Stream> or indicator components watch the same pattern.

import { TopicPresenceProvider, useTopicPresence } from "@adamo/react";
function StreamIndicator({ topic }: { topic: string }) {
const { isActive } = useTopicPresence();
return (
<span style={{ color: isActive(topic) ? "green" : "gray" }}>
{isActive(topic) ? "Live" : "Offline"}
</span>
);
}
function StreamGrid({ org }: { org: string }) {
return (
<TopicPresenceProvider pattern={`adamo/${org}/*/video/*`} ttlMs={5000}>
<StreamIndicator topic={`adamo/${org}/arm-01/video/main`} />
<StreamIndicator topic={`adamo/${org}/arm-02/video/main`} />
</TopicPresenceProvider>
);
}
PropTypeDefaultDescription
patternstringrequiredKey expression pattern to watch.
ttlMsnumber5000Milliseconds before a topic is considered stale.
childrenReact.ReactNoderequired
type TopicPresenceContextValue = {
activeTopics: Set<string>; // All currently active topic key expressions
isActive: (topic: string) => boolean; // Check a specific topic
};
import { keyExprIncludes, keyExprIntersects } from "@adamo/react";
// Does pattern "adamo/*/video/**" include the concrete topic?
keyExprIncludes("adamo/*/video/**", "adamo/arm-01/video/main");
// => true
// Do two patterns overlap?
keyExprIntersects("adamo/arm-01/**", "adamo/*/video/*");
// => true
import { json } from "@adamo/react";
// Encode an object to Uint8Array (UTF-8 JSON)
const bytes = json.encode({ position: [1.0, 2.0, 3.0] });
// Decode Uint8Array back to a typed object
const msg = json.decode<{ position: number[] }>(bytes);

@adamo/react re-exports the following types and values from the underlying transport library for convenience:

import {
// Types
type Session,
type Sample,
type Subscriber,
type Publisher,
type KeyExpr,
// Enums
Priority,
CongestionControl,
Reliability,
SampleKind,
} from "@adamo/react";

Priority values (lower = higher priority):

  • Priority.REAL_TIME — Use for control and video
  • Priority.INTERACTIVE_HIGH
  • Priority.INTERACTIVE_LOW
  • Priority.DATA_HIGH
  • Priority.DATA — Default for most application data
  • Priority.DATA_LOW
  • Priority.BACKGROUND

CongestionControl values:

  • CongestionControl.DROP — Drop messages when the network is congested (use for teleop, video, and telemetry)
  • CongestionControl.BLOCK — Block until the message can be sent (use for configuration and must-arrive commands)

Reliability values:

  • Reliability.BEST_EFFORT — No retransmission (use for real-time streams)
  • Reliability.RELIABLE — Guarantee delivery (use for configuration and must-arrive commands)

SampleKind values:

  • SampleKind.PUT — A normal publication
  • SampleKind.DELETE — A deletion (used by liveliness to signal disconnection)

A full, copy-pasteable teleoperation component that discovers robots, shows tracks, streams video, and accepts gamepad input:

import { useState } from "react";
import {
AdamoProvider,
useRobots,
useTracks,
useTrackStatus,
Stream,
GamepadController,
} from "@adamo/react";
import type { StreamStats } from "@adamo/react";
const ROUTER_URL = "wss://router.adamohq.com:443";
const ORG = "my-org";
// Top-level: wrap your app in AdamoProvider
export default function TeleoperationApp() {
return (
<AdamoProvider url={ROUTER_URL} org={ORG}>
<TeleoperationUI />
</AdamoProvider>
);
}
function TeleoperationUI() {
const robots = useRobots();
const [selectedRobot, setSelectedRobot] = useState<string | null>(null);
const [selectedTrack, setSelectedTrack] = useState("main");
const [stats, setStats] = useState<StreamStats | null>(null);
return (
<div style={{ display: "flex", height: "100vh", fontFamily: "sans-serif" }}>
{/* Sidebar: robot and track selection */}
<aside style={{ width: 200, padding: 16, borderRight: "1px solid #ccc", overflowY: "auto" }}>
<h3>Robots</h3>
{robots.length === 0 && <p style={{ color: "#888" }}>Scanning...</p>}
{robots.map((robot) => (
<button
key={robot.id}
onClick={() => {
setSelectedRobot(robot.id);
setSelectedTrack("main");
}}
style={{
display: "block",
width: "100%",
marginBottom: 4,
padding: "6px 8px",
background: selectedRobot === robot.id ? "#0066cc" : "#eee",
color: selectedRobot === robot.id ? "white" : "black",
border: "none",
borderRadius: 4,
cursor: "pointer",
textAlign: "left",
}}
>
{robot.id}
<span style={{ float: "right", fontSize: 10 }}>
{robot.isStreaming ? "online" : "offline"}
</span>
</button>
))}
{selectedRobot && (
<>
<h3>Tracks</h3>
<TrackList
robotId={selectedRobot}
selectedTrack={selectedTrack}
onSelect={setSelectedTrack}
/>
</>
)}
</aside>
{/* Main: video + stats */}
<main style={{ flex: 1, display: "flex", flexDirection: "column", background: "#111" }}>
{selectedRobot ? (
<>
<div style={{ flex: 1, position: "relative" }}>
<Stream
robot={selectedRobot}
track={selectedTrack}
onStats={setStats}
style={{ width: "100%", height: "100%" }}
/>
{stats && (
<div style={{
position: "absolute", bottom: 8, left: 8,
background: "rgba(0,0,0,0.6)", color: "white",
padding: "4px 8px", borderRadius: 4, fontSize: 12,
}}>
{stats.fps.toFixed(1)} fps | {stats.kbps.toFixed(0)} kbps |
RTT {stats.rttMs.toFixed(0)} ms | jitter {stats.jitterMs.toFixed(1)} ms
</div>
)}
</div>
{/* Gamepad input — renders nothing, just publishes */}
<GamepadController
topic={`adamo/${ORG}/${selectedRobot}/control/joy`}
config={{ deadzone: 0.05 }}
/>
</>
) : (
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "#666" }}>
Select a robot to begin
</div>
)}
</main>
</div>
);
}
function TrackList({
robotId,
selectedTrack,
onSelect,
}: {
robotId: string;
selectedTrack: string;
onSelect: (name: string) => void;
}) {
const tracks = useTracks(robotId);
const statuses = useTrackStatus(robotId);
return (
<div>
{tracks.map((track) => {
const status = statuses.get(track.name);
return (
<button
key={track.name}
onClick={() => onSelect(track.name)}
style={{
display: "block",
width: "100%",
marginBottom: 4,
padding: "6px 8px",
background: selectedTrack === track.name ? "#004499" : "#ddd",
color: selectedTrack === track.name ? "white" : "black",
border: "none",
borderRadius: 4,
cursor: "pointer",
textAlign: "left",
}}
>
{track.name}
{status && (
<span style={{ float: "right", fontSize: 10, color: status.status === "error" ? "red" : "inherit" }}>
{status.status}
</span>
)}
</button>
);
})}
</div>
);
}

All topics follow the pattern adamo/{org}/{robot}/.... The {org} segment is the organization slug and {robot} is the robot identifier.

TopicDirectionDescription
adamo/{org}/{robot}/aliveRobot → CloudLiveliness token. Present when robot is connected. Disappears on disconnect.
adamo/{org}/{robot}/heartbeatRobot → CloudPublished at 1 Hz. JSON payload listing currently active track names.
adamo/{org}/{robot}/stats/systemRobot → CloudSystem health: CPU usage, memory, temperature, etc.
TopicDirectionDescription
adamo/{org}/{robot}/video/{track}Robot → CloudVideo stream data. Binary, multiplexed: byte 0 = 0x00 (video packet) or 0x01 (FEC parity packet).
adamo/{org}/{robot}/video/{track}/aliveRobot → CloudLiveliness token for this track. Present while the track is actively streaming.
adamo/{org}/{robot}/video/{track}/metaRobot → CloudTrack metadata JSON: { "stereo": bool }. Queryable (persisted by the router).
adamo/{org}/{robot}/video/{track}/encoderUI → RobotEncoder control commands (keyframe requests, resize hints). Binary protocol.
adamo/{org}/{robot}/stats/ccUI → RobotTWCC (Transport-wide Congestion Control) feedback. Used for adaptive bitrate.
adamo/{org}/{robot}/stats/pingUI → RobotLatency ping. 4-byte request ID.
adamo/{org}/{robot}/stats/pongRobot → UILatency pong. Echoes request ID.
adamo/{org}/{robot}/video/{track}/nackUI → RobotNACK retransmission requests. Binary: [count: u16 BE][seq_0: u64 BE]...
adamo/{org}/{robot}/video/{track}/keepaliveUI → RobotSent on component mount to defer pipeline teardown during navigation.
TopicDirectionDescription
adamo/{org}/{robot}/controlUI → RobotDefault control topic (e.g., a Joy message for a simple robot).
adamo/{org}/{robot}/control/{name}UI → RobotNamed control channel. {name} is typically "joy", "joint_cmd", etc.
TopicDirectionDescription
adamo/{org}/{robot}/tracks/requestUI → RobotStart/stop track requests. JSON-encoded TrackRequestParams.
adamo/{org}/{robot}/tracks/statusRobot → UITrack lifecycle events. JSON-encoded TrackStatusEvent.

key expressions support two wildcards:

WildcardMeaning
*Matches exactly one path segment (no / characters)
**Matches any number of path segments (including zero, and including /)

Examples:

adamo/my-org/*/alive — All robot liveliness tokens in an org
adamo/my-org/arm-01/video/* — All tracks on arm-01
adamo/my-org/*/video/*/alive — All track liveliness across all robots
adamo/my-org/** — Everything under an org (use with care)

Each video packet on adamo/{org}/{robot}/video/{track} has the following layout:

Byte 0: Packet type
0x00 = video data
0x01 = FEC parity
Bytes 1+: Packet body (for type 0x00):
[0..8) seq: u64 big-endian — monotonically increasing sequence number
[8..16) timestampUs: u64 big-endian — sender clock in microseconds
[16..) data: H.264 Annex B bitstream

You do not need to parse this manually when using <Stream> — the @adamo/media package handles it internally.

{
"activeTracks": ["main", "kinect_rgb"]
}
{
"track": "kinect_rgb",
"status": "running",
"timestamp": 1709123456789
}

Status transitions: startingrunning, or starting / runningerrorstopped.

11. @adamo/fleet — Imperative Topic Utilities

Section titled “11. @adamo/fleet — Imperative Topic Utilities”

Use these when you need topic discovery outside of React (e.g., in a vanilla JS context or a Tauri command handler).

Creates a subscription that tracks which topics under a pattern are currently active. Calls your callback whenever the list changes.

import { watchTopics } from "@adamo/fleet";
const handle = await watchTopics(session, "adamo/my-org/*/video/*", { ttlMs: 5000 });
handle.onUpdate((topics) => {
console.log("Active video topics:", topics);
});
// Later:
handle.close();

Subscribes to a pattern and calls your callback for every received message.

import { watchData } from "@adamo/fleet";
const handle = await watchData(session, "adamo/my-org/arm-01/stats/system");
handle.onMessage((value) => {
const stats = JSON.parse(new TextDecoder().decode(value.payload));
console.log("CPU:", stats.cpu, "received at:", value.receivedAt);
});
handle.close();

Like watchData, but also maintains a map of the latest value for every matching topic. Supports both streaming callbacks and synchronous snapshots.

import { watchLatest } from "@adamo/fleet";
const handle = await watchLatest(session, "adamo/my-org/*/heartbeat");
// Streaming
handle.onMessage((value) => {
console.log("heartbeat from:", value.topic);
});
// Snapshot of all latest values
const latest = handle.get(); // FleetAggregate[]
for (const { topic, value } of latest) {
console.log(topic, JSON.parse(new TextDecoder().decode(value.payload)));
}
handle.close();
type FleetValue = {
topic: string; // Full key expression that published
payload: Uint8Array; // Raw message bytes
receivedAt: number; // performance.now() timestamp
};
type FleetAggregate = {
topic: string;
value: FleetValue;
};
type FleetOptions = {
ttlMs?: number; // Topic staleness TTL in milliseconds (default: 5000)
channelSize?: number; // ring buffer size (default: 16)
};

Most applications should use <Stream> from @adamo/react instead. These low-level APIs are for advanced use cases such as custom rendering pipelines, XR/VR, or non-React environments.

The main entry point. Manages the full pipeline: subscription, FEC recovery, TWCC feedback, NACK retransmission, H.264 decoding via WebCodecs, and rendering to a canvas.

import { createStreamPlayer } from "@adamo/media";
const player = await createStreamPlayer({
session,
org: "my-org",
robot: "arm-01",
track: "main",
canvas: document.getElementById("video-canvas") as HTMLCanvasElement,
fec: { type: "reed-solomon" },
onStats: (stats) => console.log(stats),
});
// Later:
player.requestKeyframe();
player.close();
type StreamPlayerOptions = {
session: Session;
robot?: string; // Robot ID (default: "robot")
track?: string; // Track name (default: "zed_video")
org?: string; // Organization slug
videoTopic?: string; // Override the full video topic (skips org/robot/track)
canvas: HTMLCanvasElement;
viewerId?: number; // Viewer ID for encoder resize hints (default: 0)
fec?: {
type: "reed-solomon";
dataShards?: number; // Default: 4
parityShards?: number; // Default: 2
};
onStats?: (stats: StreamStats) => void;
statsIntervalMs?: number; // Stats callback interval (default: 1000ms)
nack?: boolean; // Enable NACK retransmission (default: false)
decoderCodec?: string; // H.264 codec string (default: "avc1.42E01E")
resize?: {
auto?: boolean; // Auto-send resize on canvas resize (default: true)
debounceMs?: number; // Resize debounce (default: 250ms)
};
};
type StreamPlayerHandle = {
requestKeyframe: () => Promise<void>; // Ask the robot to send a keyframe
sendResize: (width: number, height: number) => Promise<void>; // Tell the encoder your display size
close: () => void; // Tear down all subscriptions and the decoder
};

Extends createStreamPlayer with WebXR immersive-vr rendering. Decodes top/bottom stereo H.264 and renders left/right eye halves through a GLSL shader. Provides a 2D canvas preview when not in VR.

import { createXRStereoPlayer } from "@adamo/media";
const player = await createXRStereoPlayer({
session,
org: "my-org",
robot: "arm-01",
track: "stereo",
canvas: previewCanvas,
convergence: 0.1,
onEnterVR: () => console.log("entered VR"),
onExitVR: () => console.log("exited VR"),
});
await player.enterVR();
player.setConvergence(0.08); // Adjust stereo depth
player.close();