# RTMS - Connection

WebSocket connection protocol details.

## Connection Flow

```
1. Receive meeting/webinar/session.rtms_started webhook
           ↓
2. Extract server_urls, stream_id, and meeting_uuid or session_id
           ↓
3. Generate signature (HMAC-SHA256) using meeting_uuid or session_id
           ↓
4. Connect to signaling WebSocket
           ↓
5. Send handshake request (msg_type 1)
           ↓
6. Receive handshake response (msg_type 2) with media server URL
           ↓
7. Connect to media WebSocket(s)
           ↓
8. Send media handshake (msg_type 3)
           ↓
9. Receive media handshake response (msg_type 4)
           ↓
10. Send ready to receive (msg_type 7)
           ↓
11. Receive media data (msg_type 14-18)
           ↓
12. Respond to heartbeats (msg_type 12 → 13)
           ↓
13. Optionally react to `PARTICIPANT_VIDEO_ON/OFF`, send `VIDEO_SUBSCRIPTION_REQ`, or gracefully terminate with `STREAM_CLOSE_REQ`
```

## Signature Generation

```javascript
const crypto = require('crypto');

// For meetings and webinars: use meeting_uuid
// For Video SDK: use session_id
// Webinars still use meeting_uuid (NOT webinar_uuid)
function generateSignature(clientId, idValue, streamId, clientSecret) {
  const message = `${clientId},${idValue},${streamId}`;
  return crypto.createHmac('sha256', clientSecret).update(message).digest('hex');
}

// Extract the correct ID from any product's webhook payload
const idValue = payload.meeting_uuid || payload.session_id;
```

## Signaling Message Types

| msg_type | Name | Direction | Description |
|----------|------|-----------|-------------|
| 1 | Handshake Request | Client → Server | Initiate connection |
| 2 | Handshake Response | Server → Client | Returns media server URL |
| 3 | Media Handshake Request | Client → Server | Request specific media types |
| 4 | Media Handshake Response | Server → Client | Confirms media subscription |
| 7 | Ready to Receive | Client → Server | Signal ready for data |
| 12 | Keep Alive Request | Server → Client | Heartbeat ping |
| 13 | Keep Alive Response | Client → Server | Heartbeat pong |

## Media Message Types

| msg_type | Media Type |
|----------|------------|
| 14 | Audio |
| 15 | Video |
| 16 | Screen Share |
| 17 | Transcript |
| 18 | Chat |

## Critical Gotchas

### 1. Only ONE Connection Per Stream!

```javascript
// WRONG - Connecting twice kicks out first connection
connectToRTMS(serverUrl, streamId);  // Connection 1
connectToRTMS(serverUrl, streamId);  // Connection 2 - kicks out Connection 1!

// CORRECT - Only connect once
if (!activeConnections.has(streamId)) {
  connectToRTMS(serverUrl, streamId);
  activeConnections.set(streamId, ws);
}
```

### 2. Heartbeat is MANDATORY

When you receive msg_type 12, you MUST respond with msg_type 13:

```javascript
ws.on('message', (data) => {
  const msg = JSON.parse(data);
  
  if (msg.msg_type === 12) {  // Keep Alive Request
    ws.send(JSON.stringify({ 
      msg_type: 13,  // Keep Alive Response
      timestamp: msg.timestamp 
    }));
  }
});
```

### 3. Reconnection is YOUR Responsibility

RTMS does NOT auto-reconnect. Implement your own retry logic:

| Server Type | Timeout |
|-------------|---------|
| Media Server | 65 seconds keep-alive tolerance before timeout |
| Signaling Server | 60 seconds to reconnect |

```javascript
ws.on('close', () => {
  // Implement exponential backoff
  setTimeout(() => reconnect(), retryDelay);
  retryDelay = Math.min(retryDelay * 2, 30000);
});
```

## Transcript LID Control

The transcript media handshake now supports explicit Language Identification control.

```javascript
mediaWs.send(JSON.stringify({
  msg_type: 3,
  protocol_version: 1,
  meeting_uuid: idValue,
  rtms_stream_id: streamId,
  signature,
  media_type: 8, // TRANSCRIPT
  media_params: {
    transcript: {
      content_type: 5,   // TEXT
      src_language: 9,   // English
      enable_lid: false  // Lock to src_language instead of auto-switching
    }
  }
}));
```

Use `enable_lid: false` when:

- the meeting should stay on a known language
- language-switching is undesirable
- you want more predictable downstream transcript processing

## Single Individual Video Subscription Flow

RTMS now supports subscribing to **one participant camera stream at a time**.

1. Open a video media socket with `data_opt = VIDEO_SINGLE_INDIVIDUAL_STREAM`
2. Subscribe to `PARTICIPANT_VIDEO_ON` and `PARTICIPANT_VIDEO_OFF`
3. When an event arrives, choose the `user_id` you want
4. Send `VIDEO_SUBSCRIPTION_REQ` on the signaling socket
5. Wait for `VIDEO_SUBSCRIPTION_RESP`
6. Expect the newest successful subscription to replace the previous participant stream

```javascript
// Signaling socket: subscribe to control-plane events
signalingWs.send(JSON.stringify({
  msg_type: 5, // EVENT_SUBSCRIPTION
  events: [
    { event_type: 8, subscribe: true }, // PARTICIPANT_VIDEO_ON
    { event_type: 9, subscribe: true }  // PARTICIPANT_VIDEO_OFF
  ]
}));

// Signaling socket: select a participant stream
signalingWs.send(JSON.stringify({
  msg_type: 28, // VIDEO_SUBSCRIPTION_REQ
  user_id: selectedUserId,
  subscribe: true,
  timestamp: Date.now()
}));
```

The March 2026 changelog did not publish the numeric values for the new message types. Use the protocol definitions before hard-coding them.

## Graceful Stream Closure

The backend can now request clean shutdown over the signaling socket:

```javascript
signalingWs.send(JSON.stringify({
  msg_type: 21, // STREAM_CLOSE_REQ
  rtms_stream_id: streamId
}));
```

Expect:

- `STREAM_CLOSE_RESP`
- then normal connection shutdown / cleanup

Use this when your app wants deterministic teardown instead of waiting for a stop webhook or socket failure.

## Split vs Unified Mode

| Mode | Description | Best For |
|------|-------------|----------|
| **Split** | One connection per media type | Most use cases. Media server supports multiple connections with different media types |
| **Unified** | One connection for all media | Real-time audio+video streaming/muxing where sync matters |

## Low-Level Connection Example

```javascript
const WebSocket = require('ws');
const crypto = require('crypto');

async function connectRTMS(webhookPayload) {
  const { server_urls, rtms_stream_id } = webhookPayload;
  // meeting_uuid for meetings/webinars, session_id for Video SDK
  const idValue = webhookPayload.meeting_uuid || webhookPayload.session_id;
  
  // Generate signature
  const signature = crypto
    .createHmac('sha256', process.env.ZOOM_CLIENT_SECRET)
    .update(`${process.env.ZOOM_CLIENT_ID},${idValue},${rtms_stream_id}`)
    .digest('hex');
  
  // Connect to signaling server
  const signalingWs = new WebSocket(server_urls, {
    headers: {
      'X-Zoom-RTMS-Stream-Id': rtms_stream_id,
      'X-Zoom-RTMS-Signature': signature
    }
  });
  
  signalingWs.on('open', () => {
    // Send handshake request
    signalingWs.send(JSON.stringify({
      msg_type: 1,
      protocol_version: 1,
      client_id: process.env.ZOOM_CLIENT_ID,
      meeting_uuid: idValue,          // Works for both meeting_uuid and session_id
      stream_id: rtms_stream_id,
      signature: signature,
      media_type: 9  // AUDIO(1) | TRANSCRIPT(8)
    }));
  });
  
  signalingWs.on('message', (data) => {
    const msg = JSON.parse(data);
    
    switch (msg.msg_type) {
      case 2:  // Handshake response
        // Connect to media server from msg.media_server_url
        connectMediaServer(msg.media_server_url);
        break;
      case 12:  // Keep alive request
        signalingWs.send(JSON.stringify({ msg_type: 13, timestamp: msg.timestamp }));
        break;
    }
  });
  
  signalingWs.on('error', (error) => {
    console.error('Signaling error:', error);
  });
  
  signalingWs.on('close', (code, reason) => {
    console.log('Signaling closed:', code, reason);
    // Implement reconnection logic
  });
}
```

## Resources

- **RTMS_CONNECTION_FLOW.md**: https://github.com/zoom/rtms-samples/blob/main/RTMS_CONNECTION_FLOW.md
- **ARCHITECTURE.md**: https://github.com/zoom/rtms-samples/blob/main/ARCHITECTURE.md
- **TROUBLESHOOTING.md**: https://github.com/zoom/rtms-samples/blob/main/TROUBLESHOOTING.md
