When a modality does not fit, work with raw tracks yourself. You publish a named track. Any number of subscribers receive it live over QUIC. A robot, an agent, a browser, and a recorder can all consume the same stream. You do not track who listens. Tracks use MoQT (Media-over-QUIC Transport). They fan out through the relay at no extra cost. You use a MoqtClient. Every SDK ships it, with method names in that language’s idiomatic case. You publish and subscribe the same way in each SDK. The relay handles connect, backoff, and reconnect for you.

Track kinds

A track is addressed by a namespace + name (e.g. robot/turtlebot4-001
  • odom). A track also carries a capability string. A capability is an open-ended routing intent ("asr", "tts", "ros.telemetry", "media.passthrough"). The relay / gateway routes the track to whatever registered that capability. You route on intent, not on media kind.

Connect

connect returns immediately and dials in the background. If the link drops, the client auto-reconnects with capped exponential backoff. It re-establishes every publication and subscription on each reconnect. Your code does nothing. on_state reports the lifecycle: You may publish or subscribe before the session is up. The client queues the calls and replays them after connection. Also, the relay holds a subscribe for a namespace that is not yet announced. Thus a robot can subscribe to its command track before any controller connects.

Publish & subscribe a frame track

Frame tracks carry opaque binary objects with a per-frame priority. They are the correct fit for robot telemetry and game state. The example below shows a publisher that streams a track and a subscriber that receives it. The relay fans the track out.
Keep the subscription handle alive for as long as you want frames. In garbage-collected languages (Python/Java/C#), the handle owns the native callback. If the handle is collected, the engine calls into freed memory. The publication handle owns the track. Drop the handle (or call close) to stop the publication.

Audio tracks

Audio tracks have the same shape, with codec metadata instead of a priority. The methods are publish_audio(ns, name, capability, sample_rate, channels, frame_ms) and subscribe_audio(ns, name, on_frame). on_frame(ts_us, bytes) delivers one decoded object. Use audio tracks for live voice between an SDK client and an agent. The capability (“asr”, “tts”, …) routes the track to the correct module.

Namespacing & routing

  • Telemetry vs commands: give command (inbound) tracks a distinct namespace from telemetry. For example, publish telemetry under robot/<id> and subscribe to commands under robot/<id>/ctl. If both share robot/<id>, the relay can route a command subscription to the robot’s own telemetry announce.
  • Capability is the routing key. Two publishers may use different capabilities on the same namespace. Subscribers and modules select by intent.
  • Fan-out is free. The relay copies each object to every subscriber. The publisher opens one stream per group, regardless of subscriber count.

How it works

You get one publish/subscribe API in every SDK, but two runtimes sit behind it. The C++, Python, and Go SDKs share one engine via the clutchcall_moqt_ffi native library. The TypeScript SDK is a standalone WebTransport implementation with its own queueing, late-join, and reconnect behaviour. Treat the two families as separate conformance surfaces. A result on one does not automatically carry to the other, until they are held to a common behaviour contract. The publish/subscribe APIs match, but queue depth, filter handling, and unsubscribe lifecycle can differ. See the per-language SDK reference for the full method list, and Architecture for how the relay mesh fans tracks out.