WebRTC Signaling

Docs

WebRTC carries audio, video, or data directly between browsers, but peers still need a signaling route to exchange connection details. This demo uses Agentuity-hosted WebSocket signaling to join a room. The browser RTCPeerConnection carries media and data peer-to-peer. Open the same room in another tab to connect. For server-to-client events, see SSE Stream.

Idle
You
Local participant
Voice level
Y
Local preview
Connect to start your camera and microphone preview.
Local preview
Peer
Waiting
Voice level
P
Waiting for peer
Open this room in another tab, then connect from both tabs.
Waiting for peer

Connection events and data-channel messages appear here.

Reference Code
import { Hono } from "hono";
import { createBunWebSocket } from "hono/bun";
import type { ServerWebSocket } from "bun";

const { upgradeWebSocket, websocket } =
  createBunWebSocket<ServerWebSocket>();

const rooms = new Map<string, Set<{ send(data: string): void }>>();
const app = new Hono();

app.get("/api/webrtc/signal", upgradeWebSocket((c) => {
  const roomId = c.req.query("room") ?? "default";
  const peers = rooms.get(roomId) ?? new Set();
  rooms.set(roomId, peers);

  return {
    onOpen(_event, ws) {
      if (peers.size >= 2) {
        ws.send(JSON.stringify({ type: "room-full" }));
        return;
      }

      peers.add(ws);
      ws.send(JSON.stringify({
        type: "joined",
        initiator: peers.size === 2,
      }));
    },
    onMessage(event, ws) {
      // Relay opaque SDP and ICE payloads. The server does not inspect them.
      for (const peer of peers) {
        if (peer !== ws) {
          peer.send(String(event.data));
        }
      }
    },
    onClose(_event, ws) {
      peers.delete(ws);
      if (peers.size === 0) {
        rooms.delete(roomId);
      }
    },
  };
}));

// Agentuity hosts this route. Browser WebRTC handles media and data channels.
// Create RTCPeerConnection in the client, send each offer/answer/candidate over
// /api/webrtc/signal, then pass received payloads into setRemoteDescription()
// or addIceCandidate().

export default {
  fetch: app.fetch,
  websocket,
};
Ready
Output will appear here...