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/troubleshooting/common-issues.md
# Common Issues

Troubleshooting guide for Zoom RTMS.

## Quick Diagnostics

| Symptom | Likely Cause | Solution |
|---------|--------------|----------|
| Connection fails | Invalid signature | Check signature generation |
| Duplicate connections | Slow webhook response | Respond 200 immediately |
| No data received | Wrong media type | Check media_type bitmask |
| Connection closes | Missing heartbeat | Respond to msg_type 12 |
| Segmentation fault | Old Node.js | Upgrade to 20.3.0+ |

## Connection Issues

### Webhook Response Timing

**Problem**: Random disconnections, duplicate connections

**Cause**: If your webhook handler takes too long to respond, Zoom retries. The retry creates a second connection, which kicks out the first (only 1 connection allowed per stream).

**Solution**: Respond 200 IMMEDIATELY before any processing:

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

  // Then process asynchronously
  setImmediate(() => {
    handleRTMSEvent(req.body);
  });
});

// WRONG
app.post('/webhook', async (req, res) => {
  await heavyProcessing(req.body);  // Zoom retries while waiting!
  res.status(200).send();
});
```

### Duplicate Connection Prevention

**Problem**: Multiple connections to same stream

**Solution**: Track active sessions:

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

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

  if (activeSessions.has(streamId)) {
    console.log('Already connected, ignoring duplicate');
    return;
  }

  activeSessions.set(streamId, Date.now());
  connectToRTMS(payload);
}

function handleRTMSStopped(payload) {
  activeSessions.delete(payload.rtms_stream_id);
}
```

### Invalid Signature

**Problem**: Handshake fails with status_code 3

**Cause**: Signature generation incorrect

**Solution**: Verify format:

```javascript
// Message format: "clientId,meetingUuid,streamId"
const message = `${clientId},${meetingUuid},${streamId}`;
const signature = crypto.createHmac('sha256', clientSecret)
  .update(message)
  .digest('hex');
```

**Checklist**:
- [ ] Using correct clientId (not app name)
- [ ] Using correct clientSecret
- [ ] No extra spaces in message
- [ ] Using hex output (not base64)

### Connection Timeout

**Problem**: WebSocket connection times out

**Causes**:
- Network issues
- Firewall blocking WebSocket
- Server URL expired

**Solution**:
1. Check network connectivity
2. Ensure firewall allows WSS
3. Use fresh webhook payload (don't cache URLs)

## Heartbeat Issues

### Connection Closes Unexpectedly

**Problem**: Connection closes after ~60 seconds

**Cause**: Not responding to heartbeat

**Solution**: Respond to msg_type 12 with msg_type 13:

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

  if (msg.msg_type === 12) {
    ws.send(JSON.stringify({
      msg_type: 13,
      timestamp: msg.timestamp
    }));
  }
});
```

**Timeouts**:
- Signaling: ~60 seconds
- Media: ~65 seconds

## Media Data Issues

### No Audio Data

**Causes**:
1. Wrong media_type in handshake
2. No participants speaking
3. Audio not enabled in meeting

**Solution**:
1. Verify media_type includes AUDIO (1):
   ```javascript
   media_type: 1  // Just audio
   media_type: 9  // Audio + Transcript
   media_type: 32 // All media
   ```
2. Wait for participant to speak
3. Check meeting audio settings

### No Video Data

**Causes**:
1. Wrong media_type
2. No video enabled
3. Wrong codec for FPS
4. Individual video mode enabled but no participant subscription sent

**Solution**:
1. Include VIDEO (2) in media_type
2. Use H.264 for fps > 5:
   ```javascript
   video: {
     codec: 7,        // H.264
     resolution: 2,   // HD
     fps: 25          // > 5 requires H.264
   }
   ```
3. If using `VIDEO_SINGLE_INDIVIDUAL_STREAM`, also:
   - subscribe to `PARTICIPANT_VIDEO_ON` / `PARTICIPANT_VIDEO_OFF`
   - send `VIDEO_SUBSCRIPTION_REQ` with a live `user_id`
   - remember a new request replaces the previous participant stream

### No Screen Share Data

**Problem**: Not receiving screen share even when active

**Cause**: Screen share is SEPARATE from video

**Solution**: Include DESKSHARE (4) in media_type:
```javascript
media_type: 4   // Just screen share
media_type: 5   // Audio + screen share
media_type: 32  // All media
```

### Transcript Language Delay

**Problem**: noticeable startup delay before transcription stabilizes

**Cause**: Language Identification (LID) is enabled and RTMS is auto-detecting / auto-switching languages

**Solution**: Set a source language and disable LID when you want a fixed language:
```javascript
transcript: {
  content_type: 5,
  src_language: 9,   // English
  enable_lid: false  // Fixed language, no auto-switch
}
```

**Language IDs**:
| ID | Language |
|----|----------|
| 9 | English |
| 4 | Chinese (Simplified) |
| 20 | Japanese |
| 21 | Korean |
| 28 | Spanish |

See [Data Types](../references/data-types.md#transcript-languages) for full list.

### Participant Video Events Arrive But No Video Stream Follows

**Problem**: You receive `PARTICIPANT_VIDEO_ON`, but no actual participant video frames arrive.

**Cause**: Those events only tell you whose camera is currently available. They do not automatically switch the data socket to that participant.

**Solution**:

1. open the video media socket with `VIDEO_SINGLE_INDIVIDUAL_STREAM`
2. handle `PARTICIPANT_VIDEO_ON` / `PARTICIPANT_VIDEO_OFF`
3. choose one `user_id`
4. send `VIDEO_SUBSCRIPTION_REQ`
5. wait for `VIDEO_SUBSCRIPTION_RESP`

Also remember:

- only one participant stream is supported at a time
- a newer subscription overrides the previous participant stream

### Stream Never Closes Cleanly From Backend

**Problem**: Your app finishes processing, but the RTMS stream remains open until external stop events arrive.

**Solution**: Use the new graceful-close control message on the signaling socket:

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

Treat `STREAM_CLOSE_RESP` as acknowledgement, then continue with local cleanup.

## SDK-Specific Issues

### Segmentation Fault

**Problem**: App crashes with segmentation fault

**Cause**: Node.js version < 20.3.0

**Solution**:
```bash
# Check version
node --version

# Upgrade with nvm
nvm install 24
nvm use 24

# Clear cache and reinstall
npm cache clean --force
rm -rf node_modules package-lock.json
npm install
```

### Audio Metadata Missing userId

**Problem**: `onAudioData` metadata doesn't include speaker userId

**Cause**: Using AUDIO_MIXED_STREAM (all audio combined)

**Solution**: Use `onActiveSpeakerEvent` for speaker identification:
```javascript
client.onActiveSpeakerEvent((timestamp, userId, userName) => {
  console.log(`Current speaker: ${userName}`);
});
```

Or use AUDIO_MULTI_STREAMS:
```javascript
client.setAudioParams({
  dataOpt: 2  // Per-participant streams
});
```

### Video Parameters Ignored

**Problem**: `setVideoParams` not taking effect

**Cause**: SDK bug - video params ignored after audio params

**Workaround**: Call `setVideoParams` BEFORE `setAudioParams`:
```javascript
// CORRECT ORDER
client.setVideoParams({ codec: 7, fps: 25 });
client.setAudioParams({ codec: 4, sampleRate: 3 });
client.join(payload);
```

### SDK Invalid State

**Problem**: "Invalid status" error on join

**Cause**: SDK still cleaning up from previous session

**Solution**: Retry with delay:
```javascript
try {
  client.join(payload);
} catch (error) {
  if (error.message?.includes('Invalid status')) {
    console.warn('SDK cleaning up, retrying in 2s');

    setTimeout(() => {
      client.join(payload);
    }, 2000);
  }
}
```

## Platform Issues

### Platform Not Supported

**Problem**: SDK installation fails

**Currently Supported**:
- darwin-arm64 (Apple Silicon)
- linux-x64

**Not Yet Supported**:
- Windows
- darwin-x64 (Intel Mac)
- linux-arm64

**Workaround**: Use [Manual WebSocket](../examples/manual-websocket.md) implementation.

## Status Code Reference

| Code | Name | Description |
|------|------|-------------|
| 0 | STATUS_OK | Success |
| 3 | STATUS_INVALID_SIGNATURE | Invalid signature |
| 8 | STATUS_DUPLICATE_SIGNAL_REQUEST | Already connected (signaling) |
| 16 | STATUS_DUPLICATE_MEDIA_DATA_CONNECTION | Already connected (media) |
| 40 | STATUS_INVALID_RTMS_SESSION_ID | Invalid RTMS session ID |
| 43 | STATUS_INVALID_MEDIA_TRANSCRIPT_SROUCE_LANGUAGE | Invalid transcript source language |

See [Data Types](../references/data-types.md) for complete list.

## Product-Specific Issues

### Video SDK Issues

**Problem**: Signature validation fails for Video SDK sessions

**Cause**: Using OAuth Client ID/Secret instead of SDK Key/Secret, or using `meeting_uuid` instead of `session_id`.

**Solution**:
- Video SDK apps use **SDK Key** (as `clientId`) and **SDK Secret** (as `clientSecret`).
- Video SDK webhook payloads contain `session_id`, NOT `meeting_uuid`.
- The HMAC signature must use `session_id`: `HMAC-SHA256(sdkSecret, "sdkKey,sessionId,streamId")`

```javascript
// Extract the correct ID based on product
const idValue = payload.meeting_uuid || payload.session_id;
const signature = generateSignature(clientId, idValue, streamId, clientSecret);
```

### Webinar Issues

**Problem**: Looking for `webinar_uuid` in the payload

**Cause**: Expecting a webinar-specific UUID field.

**Solution**: Webinar RTMS payloads still use `meeting_uuid` (NOT `webinar_uuid`). This is a common gotcha. The signature, connection flow, and protocol are identical to meetings.

**Problem**: Missing attendee streams in webinars

**Cause**: Webinar attendees are view-only participants.

**Solution**: Only **panelist** audio/video streams are confirmed to be available via RTMS. Attendee streams may not be available individually. Design your application to work with panelist streams only.

**Problem**: Practice session not triggering RTMS

**Cause**: Practice sessions are not documented for RTMS support.

**Solution**: RTMS events are expected when the webinar goes live to attendees, not during practice sessions. Q&A and Polls data are also not exposed via RTMS.

## Getting Help

1. **Developer Forum**: https://devforum.zoom.us/
2. **GitHub Issues**: https://github.com/zoom/rtms/issues
3. **Official Docs**: https://developers.zoom.us/docs/rtms/

## Next Steps

- **[Connection Architecture](../concepts/connection-architecture.md)** - Understand the protocol
- **[Lifecycle Flow](../concepts/lifecycle-flow.md)** - Correct connection sequence
- **[Data Types](../references/data-types.md)** - All status codes and enums