WebRTCPlugin

Adds WebRTC peer-to-peer signaling to a room. Wires the four standard signaling messages (peer list + SDP offer/answer + ICE candidate relay) and broadcasts peer-left when a connected peer disconnects.

This plugin is signaling only: it handles the SDP/ICE exchange needed to establish WebRTC connections. The actual audio/video/data streams flow directly between peers. Colyseus state synchronization continues to work through the server as usual.

Installation

The server-side plugin ships with the colyseus package: no extra install needed.

VideoRoom.ts
import { Room, definePlugins } from "colyseus";
import { WebRTCPlugin } from "colyseus/plugins/webrtc";
 
export class VideoRoom extends Room {
  plugins = definePlugins([
    new WebRTCPlugin(),
  ]);
}

The client-side helper ships in a separate browser-only package:

npm install @colyseus/webrtc
⚠️

WebRTC SDP payloads can exceed the default maxPayload of 4KB. Increase it on your transport when adding the plugin:

server.ts
import { WebSocketTransport } from "@colyseus/ws-transport";
 
const server = defineServer({
  transport: new WebSocketTransport({ maxPayload: 16 * 1024 }),
  // ...
});

Signaling messages

The plugin contributes the following messages to your room. You can listen to them or send them yourself if you need to extend the signaling flow.

MessageDirectionDescription
webrtc:joinclient → serverClient opts into the mesh. Server replies with the current peer list and broadcasts to others.
webrtc:offerclient → server → clientRelay SDP offer to the target peer.
webrtc:answerclient → server → clientRelay SDP answer to the target peer.
webrtc:ice-candidateclient → server → clientRelay ICE candidate to the target peer.
webrtc:peersserver → clientList of existing peer session IDs (sent on join).
webrtc:peer-joinedserver → clientA new peer has joined the mesh.
webrtc:peer-leftserver → clientA peer has left the mesh. Only emitted for clients that opted in via webrtc:join.

The plugin tracks which clients have explicitly opted into the mesh. Spectators and other non-WebRTC clients in the same room therefore don’t appear in peer events.

Client-side usage

Use WebRTCClient from @colyseus/webrtc/client to consume the signaling and manage RTCPeerConnection instances.

The source is a single ~170-line file you can read or fork. See the reference implementation on GitHub.

client.ts
import { Client } from "@colyseus/sdk";
import { WebRTCClient } from "@colyseus/webrtc/client";
 
const client = new Client("ws://localhost:2567");
const room = await client.joinOrCreate("video");
 
const webrtc = new WebRTCClient(room);
 
webrtc.onLocalStream = (stream) => {
  // Attach to a <video> element for self-preview
  localVideo.srcObject = stream;
};
 
webrtc.onPeerConnected = (peerId, stream) => {
  // Attach remote stream to a <video> element
};
 
webrtc.onPeerDisconnected = (peerId) => {
  // Remove the peer's video element
};
 
// Request camera/mic and start signaling
await webrtc.join({ audio: true, video: true });
 
// Later, to stop and clean up:
webrtc.leave();
room.leave();

WebRTCClient API

Constructor

new WebRTCClient(room, options?)
  • room: A Colyseus Room instance (or any object exposing send(type, message) and onMessage(type, callback)).
  • options.iceServers: Custom ICE servers. Defaults to Google’s public STUN servers.

Methods

MethodDescription
join(constraints?)Request user media with the given MediaStreamConstraints and start signaling. Defaults to { audio: true, video: true }.
leave()Close all peer connections, stop local tracks, and unbind signaling listeners.

Callbacks

CallbackSignatureDescription
onLocalStream(stream: MediaStream) => voidFired after getUserMedia succeeds.
onPeerConnected(peerId: string, stream: MediaStream) => voidFired when a remote peer’s media stream is received.
onPeerDisconnected(peerId: string) => voidFired when a peer connection closes.

Properties

PropertyTypeDescription
localStreamMediaStream | nullThe local media stream.
peersMap<string, RTCPeerConnection>Active peer connections keyed by session ID.
streamsMap<string, MediaStream>Remote media streams keyed by session ID.

Combining with your own messages

The plugin’s messages are merged into the room’s messages map. You can define your own alongside without any conflict:

VideoRoom.ts
import { Room, Client, definePlugins } from "colyseus";
import { WebRTCPlugin } from "colyseus/plugins/webrtc";
 
export class VideoRoom extends Room {
  plugins = definePlugins([
    new WebRTCPlugin(),
  ]);
 
  messages = {
    chat: (client: Client, message: string) => {
      this.broadcast("chat", `${client.sessionId}: ${message}`);
    },
  };
}

See Room Plugins → Message handler rules for the conflict resolution semantics.

How it works

Client A                     Server                      Client B
   |                            |                            |
   |---- webrtc:join ---------->|                            |
   |<--- webrtc:peers (B) ------|                            |
   |                            |---- webrtc:peer-joined --->|
   |---- webrtc:offer (B) ----->|---- webrtc:offer (A) ----->|
   |<--- webrtc:answer (B) -----|<--- webrtc:answer (A) -----|
   |---- webrtc:ice-candidate ->|---- webrtc:ice-candidate ->|
   |                            |                            |
   |<======================= RTCPeerConnection =============>|
                          (direct, P2P)

Once signaling completes, audio/video/data flows directly between the browsers via RTCPeerConnection. It doesn’t go through your Colyseus server.

See Also