HEX
Server: Apache/2.4.46 (Win64) OpenSSL/1.1.1j PHP/8.4.25
System: Windows NT DESKTOP-4TAV2RJ 10.0 build 19045 (Windows 10) AMD64
User: fred (0)
PHP: 8.4.25
Disabled: NONE
Upload Files
File: C:/Users/fred/.codex/.tmp/plugins/plugins/zoom/skills/rtms/concepts/lifecycle-flow.md
# RTMS Lifecycle Flow

Complete flow from meeting/webinar/session start to media streaming.

## High-Level Flow

```
┌─────────────────────────────┐
│  Meeting/Webinar/Session    │
│  Starts                     │
└────────────┬────────────────┘
             │
             ▼
┌─────────────────────────────┐
│  Zoom sends webhook event   │
│  meeting.rtms_started  OR   │
│  webinar.rtms_started  OR   │
│  session.rtms_started       │
└────────────┬────────────────┘
         │
         ▼
┌──────────────────┐
│  Your server     │
│  receives        │
│  webhook         │
│                  │
│  RESPOND 200     │
│  IMMEDIATELY!    │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  Connect to      │
│  Signaling WS    │
│                  │
│  Send handshake  │
│  (msg_type: 1)   │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  Receive         │
│  handshake resp  │
│  (msg_type: 2)   │
│                  │
│  Extract media   │
│  server URL      │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  Connect to      │
│  Media WS        │
│                  │
│  Send handshake  │
│  (msg_type: 3)   │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  Receive media   │
│  handshake resp  │
│  (msg_type: 4)   │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  Send Client     │
│  Ready to        │
│  Signaling       │
│  (msg_type: 7)   │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  Receive media   │
│  data:           │
│  - Audio (14)    │
│  - Video (15)    │
│  - Share (16)    │
│  - Transcript(17)│
│  - Chat (18)     │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  Respond to      │
│  heartbeats      │
│  (12 -> 13)      │
└────────┬─────────┘
         │
         ▼
┌─────────────────────────────┐
│  Optional control-plane     │
│  actions during stream      │
│  - EVENT_SUBSCRIPTION       │
│  - VIDEO_SUBSCRIPTION_REQ   │
│  - STREAM_CLOSE_REQ         │
└────────────┬────────────────┘
             │
             ▼
┌─────────────────────────────┐
│  meeting/webinar/session    │
│  .rtms_stopped              │
│                             │
│  Close sockets              │
│  Cleanup                    │
└─────────────────────────────┘
```

## Detailed Steps

### Step 1: Receive Webhook

When RTMS starts, Zoom sends a webhook. The event name and payload differ by product:

**Meeting RTMS:**
```json
{
  "event": "meeting.rtms_started",
  "payload": {
    "account_id": "abc123",
    "object": {
      "meeting_id": "123456789",
      "meeting_uuid": "AbC123...",
      "host_id": "user123",
      "rtms_stream_id": "stream123==",
      "server_urls": "wss://rtms-sjc1.zoom.us/...",
      "signature": "pre_computed_signature"
    }
  }
}
```

**Webinar RTMS:**
```json
{
  "event": "webinar.rtms_started",
  "payload": {
    "account_id": "abc123",
    "object": {
      "meeting_id": "123456789",
      "meeting_uuid": "AbC123...",
      "host_id": "user123",
      "rtms_stream_id": "stream123==",
      "server_urls": "wss://rtms-sjc1.zoom.us/...",
      "signature": "pre_computed_signature"
    }
  }
}
```

> **Note**: Webinar payloads use `meeting_uuid`, NOT `webinar_uuid`.

**Video SDK RTMS:**
```json
{
  "event": "session.rtms_started",
  "payload": {
    "account_id": "abc123",
    "object": {
      "session_id": "SessionABC...",
      "rtms_stream_id": "stream123==",
      "server_urls": "wss://rtms-sjc1.zoom.us/...",
      "signature": "pre_computed_signature"
    }
  }
}
```

> **Note**: Video SDK payloads use `session_id` instead of `meeting_uuid`.

### Product Differences

| Aspect | Meetings | Webinars | Video SDK |
|--------|----------|----------|-----------|
| Webhook event | `meeting.rtms_started` | `webinar.rtms_started` | `session.rtms_started` |
| Payload ID field | `meeting_uuid` | `meeting_uuid` (same!) | `session_id` |
| App type | General App (OAuth) | General App (OAuth) | Video SDK App (SDK Key/Secret) |
| Participants | All participants | Panelists have full streams; attendees may not | All participants |
| Protocol after connect | Identical | Identical | Identical |

**CRITICAL**: Respond with HTTP 200 **IMMEDIATELY** before any processing!

```javascript
const RTMS_EVENTS = ['meeting.rtms_started', 'webinar.rtms_started', 'session.rtms_started'];

app.post('/webhook', (req, res) => {
  res.status(200).send();  // FIRST!

  const { event, payload } = req.body;
  if (RTMS_EVENTS.includes(event)) {
    handleRTMSStarted(payload);
  }
});
```

**Why?** If you delay, Zoom retries the webhook. The retry creates a second connection, which kicks out your first connection.

### Step 2: Connect to Signaling WebSocket

```javascript
const signalingWs = new WebSocket(payload.server_urls);

// Use meeting_uuid for meetings/webinars, session_id for Video SDK
const idValue = payload.meeting_uuid || payload.session_id;

signalingWs.on('open', () => {
  const signature = generateSignature(
    CLIENT_ID,
    idValue,
    payload.rtms_stream_id,
    CLIENT_SECRET
  );

  signalingWs.send(JSON.stringify({
    msg_type: 1,                    // Handshake request
    protocol_version: 1,
    meeting_uuid: idValue,
    rtms_stream_id: payload.rtms_stream_id,
    signature: signature,
    media_type: 9                   // Audio(1) + Transcript(8)
  }));
});
```

### Step 3: Handle Signaling Response

```javascript
signalingWs.on('message', (data) => {
  const msg = JSON.parse(data);

  switch (msg.msg_type) {
    case 2:  // Handshake response
      if (msg.status_code === 0) {
        // Extract media server URL
        const mediaUrl = msg.media_server.server_urls.all;
        connectToMediaServer(mediaUrl);
      } else {
        console.error('Handshake failed:', msg.status_code);
      }
      break;

    case 12:  // Keep alive request
      signalingWs.send(JSON.stringify({
        msg_type: 13,
        timestamp: msg.timestamp
      }));
      break;
  }
});
```

### Step 4: Connect to Media WebSocket

```javascript
function connectToMediaServer(mediaUrl) {
  const mediaWs = new WebSocket(mediaUrl);

  mediaWs.on('open', () => {
    mediaWs.send(JSON.stringify({
      msg_type: 3,                  // Media handshake request
      protocol_version: 1,
      meeting_uuid: idValue,        // meeting_uuid or session_id
      rtms_stream_id: streamId,
      signature: signature,
      media_type: 9,                // Audio + Transcript
      payload_encryption: false,
      media_params: {
        audio: {
          content_type: 2,          // RAW_AUDIO
          sample_rate: 1,           // 16kHz
          channel: 1,               // Mono
          codec: 1,                 // L16 (PCM)
          data_opt: 1,              // Mixed stream
          send_rate: 20             // 20ms chunks
        },
        transcript: {
          content_type: 5,          // TEXT
          src_language: 9,          // English
          enable_lid: false         // Fixed language, no auto-switch
        }
      }
    }));
  });
}
```

### Step 5: Start Streaming

After media handshake succeeds, tell signaling you're ready:

```javascript
mediaWs.on('message', (data) => {
  const msg = JSON.parse(data);

  if (msg.msg_type === 4 && msg.status_code === 0) {
    // Media handshake success - tell signaling we're ready
    signalingWs.send(JSON.stringify({
      msg_type: 7,                  // Client ready
      rtms_stream_id: streamId
    }));
  }
});
```

### Step 6: Receive Media Data

```javascript
mediaWs.on('message', (data) => {
  const msg = JSON.parse(data);

  switch (msg.msg_type) {
    case 14:  // Audio
      const audioBuffer = Buffer.from(msg.content, 'base64');
      processAudio(audioBuffer, msg.user_name, msg.timestamp);
      break;

    case 15:  // Video
      const videoBuffer = Buffer.from(msg.content, 'base64');
      processVideo(videoBuffer, msg.user_name, msg.timestamp);
      break;

    case 16:  // Screen share
      const shareBuffer = Buffer.from(msg.content, 'base64');
      processScreenShare(shareBuffer, msg.user_name, msg.timestamp);
      break;

    case 17:  // Transcript
      console.log(`${msg.user_name}: ${msg.content}`);
      break;

    case 18:  // Chat
      console.log(`[Chat] ${msg.user_name}: ${msg.content}`);
      break;

    case 12:  // Keep alive
      mediaWs.send(JSON.stringify({
        msg_type: 13,
        timestamp: msg.timestamp
      }));
      break;
  }
});
```

### Step 6A: Track Available Participant Video Streams

When using the new single-individual-video mode, the signaling socket tells you whose camera is currently available.

```javascript
const activeVideoUsers = new Set();

function handleEventUpdate(msg) {
  const eventType = msg.event?.event_type;
  const participants = msg.event?.participants || [];

  if (eventType === 8) { // PARTICIPANT_VIDEO_ON
    for (const participant of participants) activeVideoUsers.add(participant.user_id);
  }

  if (eventType === 9) { // PARTICIPANT_VIDEO_OFF
    for (const participant of participants) activeVideoUsers.delete(participant.user_id);
  }
}
```

Use these events as the control-plane signal for which participant video streams are currently subscribable.

### Step 6B: Select One Participant Video Stream

```javascript
function subscribeToParticipantVideo(streamId, userId) {
  const signalingWs = signalingConnections.get(streamId);
  if (!signalingWs) return;

  signalingWs.send(JSON.stringify({
    msg_type: 28, // VIDEO_SUBSCRIPTION_REQ
    user_id: userId,
    subscribe: true,
    timestamp: Date.now()
  }));
}
```

Important constraint:

- only one participant stream can be active at a time
- the newest successful subscription replaces the previous selection

### Step 7: Handle Session End

```javascript
const RTMS_STOP_EVENTS = ['meeting.rtms_stopped', 'webinar.rtms_stopped', 'session.rtms_stopped'];

// Via webhook
app.post('/webhook', (req, res) => {
  res.status(200).send();

  const { event, payload } = req.body;

  if (RTMS_STOP_EVENTS.includes(event)) {
    const streamId = payload.rtms_stream_id;

    // Close connections
    signalingConnections.get(streamId)?.close();
    mediaConnections.get(streamId)?.close();

    // Cleanup
    signalingConnections.delete(streamId);
    mediaConnections.delete(streamId);
  }
});

// Also handle WebSocket close events
signalingWs.on('close', (code, reason) => {
  console.log('Signaling closed:', code, reason);
  // Implement reconnection if needed
});
```

### Optional: Client-Initiated Graceful Close

The backend can now ask RTMS to terminate the stream cleanly:

```javascript
function closeStream(streamId) {
  const signalingWs = signalingConnections.get(streamId);
  if (!signalingWs) return;

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

Expect a `STREAM_CLOSE_RESP` followed by normal socket teardown.

## Session Tracking

**CRITICAL**: Track active sessions to prevent duplicate connections!

```javascript
const activeSessions = new Map();

function handleRTMSStarted(payload) {
  const streamId = payload.rtms_stream_id;

  // Check for existing connection
  if (activeSessions.has(streamId)) {
    console.log('Already connected to this stream, ignoring');
    return;
  }

  // Mark as active (meeting_uuid for meetings/webinars, session_id for Video SDK)
  activeSessions.set(streamId, {
    startTime: Date.now(),
    idValue: payload.meeting_uuid || payload.session_id
  });

  // Connect
  connectToRTMS(payload);
}

function handleRTMSStopped(payload) {
  const streamId = payload.rtms_stream_id;
  activeSessions.delete(streamId);
  // ... cleanup
}
```

## Error Handling

```javascript
// SDK state management (from Arlo sample)
try {
  client.join(payload);
} catch (error) {
  if (error.message?.includes('Invalid status')) {
    console.warn('SDK in invalid state, waiting to retry...');

    setTimeout(() => {
      handleRTMSStarted(payload);
    }, 2000);
  }
}
```

## Next Steps

- **[SDK Quickstart](../examples/sdk-quickstart.md)** - SDK handles all this automatically
- **[Manual WebSocket](../examples/manual-websocket.md)** - Full implementation code
- **[Common Issues](../troubleshooting/common-issues.md)** - Debugging connection problems