Skip to content

Use WebRTC Data Channels

Problem

You need ultra-low-latency peer-to-peer communication for real-time applications like collaborative drawing, live audio visualization, or multiplayer games where WebSocket round-trips through the server add too much delay.

Solution

Configure WebRTC options on the TypeScript client and use connectRTC() to establish peer-to-peer data channels. Messages sent over RTC bypass the server entirely.

INFO

WebRTC data channels are currently available in the TypeScript SDK only. Python and Swift clients can still communicate with WebRTC-enabled clients over WebSocket — messages fall back automatically when the fallback option is enabled (default).

Code

typescript
import { StarfishClient } from "@starfish/sdk";

const client = new StarfishClient({
  server: "ws://localhost:4000",
  rtc: {
    factory: RTCPeerConnection,  // browser built-in
    iceServers: [
      { urls: "stun:stun.l.google.com:19302" },
    ],
  },
});

await client.connect();
await client.join("my-session");

// Connect to a peer via WebRTC
const peerId = "client-abc";
await client.connectRTC(peerId, ["control", "stream"]);

// Send via RTC using delivery options
client.send(peerId, { cursor: { x: 100, y: 200 } }, {
  delivery: { reliability: "unreliable" },
});

// Use preferTransport to explicitly route via RTC
client.publish("cursors", { x: 100, y: 200 }, {
  delivery: {
    preferTransport: "rtc",
    reliability: "unreliable",
    fallback: true,  // fall back to WebSocket if RTC unavailable
  },
});

// Monitor RTC peer connections
client.rtcPeers$.subscribe((peers) => {
  for (const peer of peers) {
    console.log(`Peer ${peer.id}: ${peer.state}`);
  }
});

// Disconnect from a peer
client.disconnectRTC(peerId);

Explanation

Default channels

WebRTC connections include three default channels:

ChannelMax payloadUse case
control64 KBCommands, state updates
stream16 KBHigh-frequency data (cursors, audio levels)
stateState synchronization

Transport selection

Use preferTransport in delivery options to control routing:

  • "ws" — always use WebSocket (default)
  • "rtc" — prefer WebRTC data channels
  • "auto" — let the client choose based on connection availability and message characteristics

Fallback behavior

When fallback: true (default), messages intended for RTC are sent over WebSocket if no RTC connection exists to the target peer. This lets you use preferTransport: "rtc" without worrying about connection state.

Production TURN

The STUN-only iceServers config in the example above works on open networks, but restrictive NATs and firewalls — common on venue, corporate, museum, and mobile carrier networks — block direct peer-to-peer connectivity. Production apps should add a TURN relay (with credentials) so RTC can still establish:

typescript
const client = new StarfishClient({
  server: "ws://localhost:4000",
  rtc: {
    factory: RTCPeerConnection,
    iceServers: [
      { urls: "stun:stun.l.google.com:19302" },
      {
        urls: ["turn:turn.example.com:3478", "turns:turn.example.com:5349"],
        username: "1783440000:client-abc",
        credential: "9f2a1b3c4d5e6f70",
      },
    ],
  },
});

Servers can also advertise iceServers (including short-lived TURN credentials) in the connection welcome. Without a reachable relay, RTC won't establish and messages fall back to WebSocket as described above — correct, but without the low-latency path. See §13.2 "Production ICE/TURN" in protocol/spec/starfish-v0.1.md.

Mesh peer-count limits

The RTC data mesh is per-peer fan-out: sending to K peers is K separate sends, so a session of N peers costs O(N²) traffic — most of it from high-frequency lanes like cursors and pose. The mesh suits small groups; for broadcast or high-fan-out topics beyond ~16 peers, route that traffic over WebSocket (preferTransport: "ws"), which the server fans out cheaply:

typescript
// Large session: broadcast over WS, keep only targeted low-latency data on RTC
client.publish("room-state", state, {
  delivery: { preferTransport: "ws", reliability: "reliable" },
});

The 16-peer figure is an advisory warning threshold, not an enforced limit — the SDK keeps working past it. See §13.3 "Data Mesh Scaling Boundary" in protocol/spec/starfish-v0.1.md.

Variations

Auto-connect to all peers

typescript
// Automatically establish RTC with every new peer
client.peers$.subscribe((peers) => {
  for (const peer of peers) {
    client.connectRTC(peer.id).catch(() => {
      // RTC not supported by this peer — WebSocket fallback will handle it
    });
  }
});

Custom channel configuration

typescript
// Connect with custom channels
await client.connectRTC(peerId, ["audio", "video", "control"]);

// Send on custom channels via delivery options
client.send(peerId, audioLevelData, {
  delivery: { preferTransport: "rtc", reliability: "unreliable" },
});
client.send(peerId, frameData, {
  delivery: { preferTransport: "rtc", reliability: "unreliable" },
});

Mixed transport messaging

typescript
// High-frequency data over RTC
client.publish("cursor-updates", cursorData, {
  delivery: { preferTransport: "rtc", reliability: "unreliable" },
});

// Important commands over WebSocket
client.publish("game-events", { event: "round-start" }, {
  delivery: { preferTransport: "ws", reliability: "reliable" },
  priority: "high",
});

To send live video/audio (camera, mic, screen, canvas) over these same peer connections, see Share Realtime Media — it uses the WebRTC mesh to carry MediaStreams with broadcast/direct/topic addressing.