Subchapter 134.14
troubleshooting/common-issues.mdMarkdown10 KBView on GitHub
| 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+ |
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:
// 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();
});Problem: Multiple connections to same stream
Solution: Track active sessions:
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);
}Problem: Handshake fails with status_code 3
Cause: Signature generation incorrect
Solution: Verify format:
// Message format: "clientId,meetingUuid,streamId"
const message = `${clientId},${meetingUuid},${streamId}`;
const signature = crypto.createHmac('sha256', clientSecret)
.update(message)
.digest('hex');Checklist:
Problem: WebSocket connection times out
Causes:
Solution:
Problem: Connection closes after ~60 seconds
Cause: Not responding to heartbeat
Solution: Respond to msg_type 12 with msg_type 13:
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:
Causes:
Solution:
media_type: 1 // Just audio
media_type: 9 // Audio + Transcript
media_type: 32 // All mediaCauses:
Solution:
video: {
codec: 7, // H.264
resolution: 2, // HD
fps: 25 // > 5 requires H.264
}VIDEO_SINGLE_INDIVIDUAL_STREAM, also:
PARTICIPANT_VIDEO_ON / PARTICIPANT_VIDEO_OFFVIDEO_SUBSCRIPTION_REQ with a live user_idProblem: Not receiving screen share even when active
Cause: Screen share is SEPARATE from video
Solution: Include DESKSHARE (4) in media_type:
media_type: 4 // Just screen share
media_type: 5 // Audio + screen share
media_type: 32 // All mediaProblem: 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:
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 for full list.
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:
VIDEO_SINGLE_INDIVIDUAL_STREAMPARTICIPANT_VIDEO_ON / PARTICIPANT_VIDEO_OFFuser_idVIDEO_SUBSCRIPTION_REQVIDEO_SUBSCRIPTION_RESPAlso remember:
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:
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.
Problem: App crashes with segmentation fault
Cause: Node.js version < 20.3.0
Solution:
# 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 installProblem: onAudioData metadata doesn’t include speaker userId
Cause: Using AUDIO_MIXED_STREAM (all audio combined)
Solution: Use onActiveSpeakerEvent for speaker identification:
client.onActiveSpeakerEvent((timestamp, userId, userName) => {
console.log(`Current speaker: ${userName}`);
});Or use AUDIO_MULTI_STREAMS:
client.setAudioParams({
dataOpt: 2 // Per-participant streams
});Problem: setVideoParams not taking effect
Cause: SDK bug - video params ignored after audio params
Workaround: Call setVideoParams BEFORE setAudioParams:
// CORRECT ORDER
client.setVideoParams({ codec: 7, fps: 25 });
client.setAudioParams({ codec: 4, sampleRate: 3 });
client.join(payload);Problem: “Invalid status” error on join
Cause: SDK still cleaning up from previous session
Solution: Retry with delay:
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);
}
}Problem: SDK installation fails
Currently Supported:
Not Yet Supported:
Workaround: Use Manual WebSocket implementation.
| 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 for complete list.
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:
clientId) and SDK Secret (as clientSecret).session_id, NOT meeting_uuid.session_id: HMAC-SHA256(sdkSecret, "sdkKey,sessionId,streamId")// Extract the correct ID based on product
const idValue = payload.meeting_uuid || payload.session_id;
const signature = generateSignature(clientId, idValue, streamId, clientSecret);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.