Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Shyamalp16/CloudGaming/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The CloudGaming signaling protocol uses JSON messages over WebSocket to coordinate WebRTC connections. All messages are validated using Zod schemas before being forwarded to peers.

Message Validation

The server validates all incoming messages against strict schemas. Invalid messages are:
  1. Dropped silently
  2. Logged with warning level
  3. Trigger a schema-error control message to the sender
  4. Increment the signaling_schema_rejects_total metric
Non-JSON messages are rejected immediately and not processed.

Message Types

The protocol supports four primary message types:
  • offer - WebRTC session description (SDP offer)
  • answer - WebRTC session description (SDP answer)
  • candidate - ICE candidate for connection establishment
  • control - Server control messages

Offer Message

Sent by the host to initiate a WebRTC connection with a peer.

Schema

interface OfferMessage {
  type: 'offer';
  sdp: string;  // Minimum length: 1
}

Validation Rules

  • type must be exactly "offer"
  • sdp must be a non-empty string
  • sdp should be valid SDP format (starts with v=)

Example

{
  "type": "offer",
  "sdp": "v=0\r\no=- 123456789 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS stream\r\nm=video 9 UDP/TLS/RTP/SAVPF 96 97\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:abc\r\na=ice-pwd:def\r\na=ice-options:trickle\r\na=fingerprint:sha-256 AA:BB:CC:DD...\r\na=setup:actpass\r\na=mid:0\r\na=sendrecv\r\na=rtcp-mux\r\na=rtpmap:96 VP8/90000\r\na=rtpmap:97 H264/90000\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:abc\r\na=ice-pwd:def\r\na=ice-options:trickle\r\na=fingerprint:sha-256 AA:BB:CC:DD...\r\na=setup:actpass\r\na=mid:1\r\na=sendrecv\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2"
}

Usage

Client-side (JavaScript):
const peerConnection = new RTCPeerConnection(config);
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);

// Send to signaling server
ws.send(JSON.stringify({
  type: 'offer',
  sdp: offer.sdp
}));
Server behavior:
  • Validates message schema
  • Publishes to Redis: room:ROOM_ID
  • Forwards to all peers in room (except sender)
  • Logs: "Received from server: { type: offer, sdp: '...', candidate: undefined }"

Answer Message

Sent by the client in response to an offer.

Schema

interface AnswerMessage {
  type: 'answer';
  sdp: string;  // Minimum length: 1
}

Validation Rules

  • type must be exactly "answer"
  • sdp must be a non-empty string
  • sdp should be valid SDP format (starts with v=)

Example

{
  "type": "answer",
  "sdp": "v=0\r\no=- 987654321 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS stream\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:ghi\r\na=ice-pwd:jkl\r\na=ice-options:trickle\r\na=fingerprint:sha-256 EE:FF:00:11...\r\na=setup:active\r\na=mid:0\r\na=sendrecv\r\na=rtcp-mux\r\na=rtpmap:96 VP8/90000\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:ghi\r\na=ice-pwd:jkl\r\na=ice-options:trickle\r\na=fingerprint:sha-256 EE:FF:00:11...\r\na=setup:active\r\na=mid:1\r\na=sendrecv\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2"
}

Usage

Client-side (JavaScript):
ws.onmessage = async (event) => {
  const msg = JSON.parse(event.data);
  
  if (msg.type === 'offer') {
    await peerConnection.setRemoteDescription(
      new RTCSessionDescription({ type: 'offer', sdp: msg.sdp })
    );
    
    const answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);
    
    // Send answer to signaling server
    ws.send(JSON.stringify({
      type: 'answer',
      sdp: answer.sdp
    }));
  }
};
Server behavior:
  • Validates message schema
  • Publishes to Redis: room:ROOM_ID
  • Forwards to all peers in room (except sender)
  • Typically forwarded to the original offer sender

Candidate Message

Sends ICE candidates for NAT traversal and connection establishment.

Schema

interface CandidateMessage {
  type: 'candidate';
  candidate: string;        // Minimum length: 1
  sdpMid?: string;         // Optional SDP media ID
  sdpMLineIndex?: number;  // Optional SDP m-line index (>= 0)
}

Validation Rules

  • type must be exactly "candidate"
  • candidate must be a non-empty string
  • sdpMid is optional string
  • sdpMLineIndex is optional non-negative integer

Example

Host candidate:
{
  "type": "candidate",
  "candidate": "candidate:1 1 udp 2130706431 192.168.1.100 54321 typ host",
  "sdpMid": "0",
  "sdpMLineIndex": 0
}
Server reflexive candidate:
{
  "type": "candidate",
  "candidate": "candidate:2 1 udp 1694498815 203.0.113.45 12345 typ srflx raddr 192.168.1.100 rport 54321",
  "sdpMid": "0",
  "sdpMLineIndex": 0
}
Relay candidate (TURN):
{
  "type": "candidate",
  "candidate": "candidate:3 1 udp 16777215 198.51.100.20 23456 typ relay raddr 203.0.113.45 rport 12345",
  "sdpMid": "1",
  "sdpMLineIndex": 1
}
End of candidates signal:
{
  "type": "candidate",
  "candidate": "",
  "sdpMid": null,
  "sdpMLineIndex": null
}

ICE Candidate Types

TypeDescriptionPriorityUse Case
hostLocal network addressHighestDirect LAN connections
srflxServer reflexive (STUN)MediumNAT traversal
prflxPeer reflexiveMediumDiscovered during checks
relayTURN relay serverLowestFallback for restrictive NATs

Usage

Client-side (JavaScript):
peerConnection.onicecandidate = (event) => {
  if (event.candidate) {
    // Send ICE candidate to signaling server
    ws.send(JSON.stringify({
      type: 'candidate',
      candidate: event.candidate.candidate,
      sdpMid: event.candidate.sdpMid,
      sdpMLineIndex: event.candidate.sdpMLineIndex
    }));
  } else {
    // ICE gathering complete (optional signal)
    console.log('ICE gathering complete');
  }
};

// Receiving candidates
ws.onmessage = async (event) => {
  const msg = JSON.parse(event.data);
  
  if (msg.type === 'candidate') {
    if (msg.candidate) {
      await peerConnection.addIceCandidate(
        new RTCIceCandidate({
          candidate: msg.candidate,
          sdpMid: msg.sdpMid,
          sdpMLineIndex: msg.sdpMLineIndex
        })
      );
    }
  }
};
Server behavior:
  • Validates message schema
  • Publishes to Redis: room:ROOM_ID
  • Forwards to all peers in room (except sender)
  • Empty candidate string is valid (end-of-candidates)

Legacy ice-candidate Format

The client also supports an older format for backwards compatibility:
{
  "type": "ice-candidate",
  "candidate": {
    "candidate": "candidate:1 1 udp 2130706431 192.168.1.100 54321 typ host",
    "sdpMid": "0",
    "sdpMLineIndex": 0
  }
}
The ice-candidate format is not validated by the server schema. Use the candidate format for proper validation.

Control Messages

Server-to-client control and informational messages.

Schema

interface ControlMessage {
  type: 'control';
  action: string;      // Minimum length: 1
  payload?: unknown;   // Optional arbitrary data
}

Validation Rules

  • type must be exactly "control"
  • action must be a non-empty string
  • payload is optional and can be any JSON-serializable value

Control Actions

schema-error

Sent by the server when a client message fails validation. Example:
{
  "type": "control",
  "action": "schema-error"
}
Trigger conditions:
  • Invalid JSON
  • Missing required fields
  • Wrong field types
  • Empty strings where non-empty required
Client handling:
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  
  if (msg.type === 'control' && msg.action === 'schema-error') {
    console.error('Server rejected last message due to schema error');
    // Check signaling payload shapes
  }
};

peer-disconnected

Sent to all room members when a peer leaves. Example:
{
  "type": "peer-disconnected"
}
peer-disconnected uses a simplified schema without the control action field for backwards compatibility.
Server behavior:
  • Broadcast when client disconnects (WebSocket close)
  • Broadcast when client removed from Redis room
  • Published to Redis channel: room:ROOM_ID
  • Forwarded to all remaining peers
Client handling:
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  
  if (msg.type === 'peer-disconnected') {
    console.warn('Peer has disconnected');
    // Clean up peer connection
    // Update UI
    // Optionally attempt reconnection
  }
};
Example implementation:
switch (msg.type) {
  case 'peer-disconnected':
    log('Peer has disconnected. Closing connection.', 'warn');
    updateConnectionStatus('disconnected', 'Peer Left');
    
    if (ws) {
      ws.onclose = null; // Prevent auto-reconnect
      ws.close();
    }
    
    if (peerConnection) {
      peerConnection.close();
      peerConnection = null;
    }
    break;
}

Message Flow Examples

Complete Connection Establishment

1. Client connects to WebSocket
   → ws://server?roomId=game-123
   
2. Host creates offer
   Host → Server: {
     "type": "offer",
     "sdp": "v=0\r\no=..." 
   }
   
3. Server forwards to client
   Server → Client: {
     "type": "offer",
     "sdp": "v=0\r\no=..."
   }
   
4. Client creates answer
   Client → Server: {
     "type": "answer",
     "sdp": "v=0\r\no=..."
   }
   
5. Server forwards to host
   Server → Host: {
     "type": "answer",
     "sdp": "v=0\r\no=..."
   }
   
6. ICE candidates exchanged
   Host → Server: {
     "type": "candidate",
     "candidate": "candidate:1 1 udp ...",
     "sdpMid": "0",
     "sdpMLineIndex": 0
   }
   
   Server → Client: (same message)
   
   Client → Server: {
     "type": "candidate",
     "candidate": "candidate:1 1 udp ...",
     "sdpMid": "0",
     "sdpMLineIndex": 0
   }
   
   Server → Host: (same message)
   
7. WebRTC connection established
   → Data channels open
   → Media streaming begins

Disconnection Flow

1. Client disconnects (network issue, browser close, etc.)
   Client WebSocket → closes
   
2. Server detects disconnection
   → Clears heartbeat interval
   → Removes from localRooms map
   → SREM room:game-123 client:uuid
   
3. Server notifies remaining peers
   Server → All Peers: {
     "type": "peer-disconnected"
   }
   
4. Peers clean up connection
   → Close RTCPeerConnection
   → Update UI
   → Stop rendering loop

Error Scenarios

Invalid Message Type

Sent:
{
  "type": "unknown-type",
  "data": "something"
}
Result:
  • Message validation fails
  • Server logs: "Dropping invalid signaling message"
  • Server sends: { "type": "control", "action": "schema-error" }
  • Message not forwarded
  • signaling_schema_rejects_total metric incremented

Missing Required Field

Sent:
{
  "type": "offer"
  // Missing sdp field
}
Result:
  • Schema validation fails on missing sdp
  • Server logs: "Dropping invalid signaling message"
  • Server sends: { "type": "control", "action": "schema-error" }
  • Message not forwarded

Empty SDP String

Sent:
{
  "type": "answer",
  "sdp": ""
}
Result:
  • Schema validation fails (minimum length: 1)
  • Server logs: "Dropping invalid signaling message"
  • Server sends: { "type": "control", "action": "schema-error" }
  • Message not forwarded

Malformed JSON

Sent:
{type: "offer", sdp: "..."}
Result:
  • JSON parse fails
  • Server logs: "Dropping non-JSON client message"
  • No control message sent (can’t send to unparseable client)
  • Message not forwarded

Message Size Limits

Maximum message size: messageMaxBytes (default: 64KB) Oversized message behavior:
if (Buffer.byteLength(message) > config.messageMaxBytes) {
  // Message dropped silently
  log('Dropping oversized text message', { clientId, roomId });
  return;
}
Typical message sizes:
  • ICE candidate: 100-300 bytes
  • SDP offer: 2-10 KB
  • SDP answer: 2-10 KB
  • Control message: 50-100 bytes
For very large SDP descriptions (multiple codecs, many media lines), consider stripping unnecessary attributes or using SDP compression.

Rate Limiting Impact

Messages are subject to multiple rate limits:
  1. Per-client token bucket: 100 msg/10s (default)
  2. Per-IP rate limit: 500 msg/10s (default)
  3. Per-room rate limit: 1000 msg/10s (default)
When rate limited:
  • Message dropped silently
  • Server logs: "Rate limit exceeded, dropping message"
  • signaling_rate_limit_drops_total metric incremented
  • No error message sent to client
Best practice: Implement client-side rate limiting and exponential backoff.

Schema Validation Implementation

The server uses Zod for runtime type validation:
const { validateSignalingMessage } = require('./validation');

const validation = validateSignalingMessage(parsedMessage);

if (!validation.ok) {
  log('Dropping invalid signaling message', { clientId, roomId });
  
  // Send error feedback
  ws.send(JSON.stringify({ 
    type: 'control', 
    action: 'schema-error' 
  }));
  
  incSchemaRejects();
  return;
}

// Forward validated message
const validatedData = validation.data;

TypeScript Definitions

For TypeScript clients, use these type definitions:
type SignalingMessage = 
  | OfferMessage 
  | AnswerMessage 
  | CandidateMessage 
  | ControlMessage;

interface OfferMessage {
  type: 'offer';
  sdp: string;
}

interface AnswerMessage {
  type: 'answer';
  sdp: string;
}

interface CandidateMessage {
  type: 'candidate';
  candidate: string;
  sdpMid?: string;
  sdpMLineIndex?: number;
}

interface ControlMessage {
  type: 'control';
  action: string;
  payload?: unknown;
}

interface PeerDisconnectedMessage {
  type: 'peer-disconnected';
}

type IncomingMessage = SignalingMessage | PeerDisconnectedMessage;

Testing Messages

Test message validation using the WebSocket connection:
const ws = new WebSocket('ws://localhost:3002?roomId=test');

ws.onopen = () => {
  // Valid offer
  ws.send(JSON.stringify({
    type: 'offer',
    sdp: 'v=0\r\no=- 123 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\n'
  }));
  
  // Invalid (missing sdp)
  ws.send(JSON.stringify({
    type: 'offer'
  }));
  // Expect: { type: 'control', action: 'schema-error' }
  
  // Invalid (empty candidate)
  ws.send(JSON.stringify({
    type: 'candidate',
    candidate: '',
    sdpMid: '0'
  }));
  // Expect: { type: 'control', action: 'schema-error' }
};

ws.onmessage = (event) => {
  console.log('Received:', JSON.parse(event.data));
};

Build docs developers (and LLMs) love