9.3 KiB
9.3 KiB
SDK Quickstart
The fastest way to receive RTMS media using the official @zoom/rtms SDK.
Installation
# Requires Node.js 20.3.0+ (24 LTS recommended)
npm install @zoom/rtms express
Environment Setup
# .env
ZM_RTMS_CLIENT=your_client_id
ZM_RTMS_SECRET=your_client_secret
Multi-Product Support
The SDK accepts both meeting_uuid (meetings/webinars) and session_id (Video SDK) via client.join(payload) transparently. You only need to handle the different webhook event names -- the rest of the protocol is identical.
// These constants cover all RTMS products
const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"];
const RTMS_STOP_EVENTS = ["meeting.rtms_stopped", "webinar.rtms_stopped", "session.rtms_stopped"];
Minimal Example
import rtms from "@zoom/rtms";
const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"];
// Handle webhook events - SDK starts webhook server automatically
rtms.onWebhookEvent(({ event, payload }) => {
if (!RTMS_EVENTS.includes(event)) return;
const client = new rtms.Client();
client.onTranscriptData((data, timestamp, metadata) => {
const text = data.toString('utf8');
console.log(`${metadata.userName}: ${text}`);
});
// SDK handles all WebSocket complexity
// Accepts both meeting_uuid and session_id transparently
client.join(payload);
});
Complete Example with All Media Types
import rtms from "@zoom/rtms";
import fs from 'fs';
const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"];
const RTMS_STOP_EVENTS = ["meeting.rtms_stopped", "webinar.rtms_stopped", "session.rtms_stopped"];
const clients = new Map();
rtms.onWebhookEvent(({ event, payload }) => {
const streamId = payload?.rtms_stream_id;
// Handle session end (meetings, webinars, and Video SDK)
if (RTMS_STOP_EVENTS.includes(event)) {
const client = clients.get(streamId);
if (client) {
client.leave();
clients.delete(streamId);
}
return;
}
if (!RTMS_EVENTS.includes(event)) return;
// Prevent duplicate connections
if (clients.has(streamId)) {
console.log('Already connected to this stream');
return;
}
const client = new rtms.Client();
clients.set(streamId, client);
// Join confirmation
client.onJoinConfirm((reason) => {
console.log(`Joined meeting: ${reason}`);
});
// Audio data
client.onAudioData((buffer, timestamp, metadata) => {
console.log(`Audio from ${metadata.userName}: ${buffer.length} bytes`);
// Save to file, send to transcription service, etc.
});
// Video data
client.onVideoData((buffer, timestamp, trackId, metadata) => {
console.log(`Video from ${metadata.userName}: ${buffer.length} bytes`);
// H.264 NAL units or JPG/PNG frames
});
// Transcript (real-time speech-to-text from Zoom)
client.onTranscriptData((buffer, timestamp, metadata) => {
const text = buffer.toString('utf8');
console.log(`[${metadata.userName}]: ${text}`);
});
// Chat messages
client.onChatData((buffer, timestamp, metadata) => {
const text = buffer.toString('utf8');
console.log(`[Chat] ${metadata.userName}: ${text}`);
});
// Screen share
client.onShareData((buffer, timestamp, metadata) => {
console.log(`Screen share from ${metadata.userName}: ${buffer.length} bytes`);
});
// Participant events
client.onParticipantEvent((event, timestamp, participants) => {
participants.forEach(p => {
console.log(`Participant ${event}: ${p.userName}`);
});
});
// Active speaker changed
client.onActiveSpeakerEvent((timestamp, userId, userName) => {
console.log(`Active speaker: ${userName}`);
});
// Screen sharing started/stopped
client.onSharingEvent((event, timestamp, userId, userName) => {
console.log(`Sharing ${event}: ${userName}`);
});
// Session ended
client.onLeave((reason) => {
console.log(`Left meeting: ${reason}`);
clients.delete(streamId);
});
// Join the meeting
client.join(payload);
});
Configuring Audio Parameters
import rtms from "@zoom/rtms";
const client = new rtms.Client();
// Set audio parameters before joining
client.setAudioParams({
contentType: 2, // RAW_AUDIO
codec: 4, // OPUS (default)
sampleRate: 3, // 48kHz
channel: 2, // Stereo (only with OPUS)
dataOpt: 2, // AUDIO_MULTI_STREAMS (per-participant)
duration: 20, // 20ms chunks
frameSize: 960 // Samples per frame
});
client.join(payload);
Audio Parameter Options
| Parameter | Options |
|---|---|
contentType |
1=RTP, 2=RAW_AUDIO |
codec |
1=L16 (PCM), 2=G.711, 3=G.722, 4=OPUS |
sampleRate |
0=8kHz, 1=16kHz, 2=32kHz, 3=48kHz |
channel |
1=Mono, 2=Stereo (OPUS only!) |
dataOpt |
1=Mixed stream, 2=Multi-streams (per participant) |
duration |
Chunk size in ms (multiple of 20, max 1000) |
Configuring Video Parameters
client.setVideoParams({
contentType: 3, // RAW_VIDEO
codec: 7, // H.264
resolution: 2, // HD (720p)
fps: 25,
dataOpt: 3 // Single active speaker
});
Video Parameter Options
| Parameter | Options |
|---|---|
codec |
5=JPG, 6=PNG, 7=H.264 |
resolution |
1=SD (480p), 2=HD (720p), 3=FHD (1080p), 4=QHD (1440p) |
fps |
1-30 (JPG/PNG max 5, H.264 max 30) |
dataOpt |
3=Single active speaker |
With Express Webhook Handler
import rtms from "@zoom/rtms";
import express from "express";
const app = express();
app.use(express.json());
const RTMS_EVENTS = ["meeting.rtms_started", "webinar.rtms_started", "session.rtms_started"];
// Use SDK's webhook handler
app.post('/webhook', rtms.createWebhookHandler(({ event, payload }) => {
if (!RTMS_EVENTS.includes(event)) return;
const client = new rtms.Client();
client.onTranscriptData((data, timestamp, metadata) => {
console.log(`${metadata.userName}: ${data.toString('utf8')}`);
});
client.join(payload);
}, '/webhook'));
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Class-Based Approach (Multiple Connections)
For applications needing multiple concurrent connections:
import rtms from "@zoom/rtms";
// Initialize SDK once
rtms.Client.initialize();
// Create multiple clients
const client1 = new rtms.Client();
const client2 = new rtms.Client();
client1.onTranscriptData((data, ts, meta) => {
console.log(`[Meeting 1] ${meta.userName}: ${data.toString('utf8')}`);
});
client2.onTranscriptData((data, ts, meta) => {
console.log(`[Meeting 2] ${meta.userName}: ${data.toString('utf8')}`);
});
// Join different meetings
client1.join(meeting1Payload);
client2.join(meeting2Payload);
Error Handling
client.onJoinConfirm((reason) => {
if (reason !== 0) {
console.error(`Join failed with reason: ${reason}`);
// Handle error
}
});
client.onLeave((reason) => {
console.log(`Left meeting with reason: ${reason}`);
// Cleanup
clients.delete(streamId);
// Optionally reconnect
if (reason === /* unexpected disconnect */) {
setTimeout(() => reconnect(), 2000);
}
});
Python SDK
import rtms
from dotenv import load_dotenv
load_dotenv()
RTMS_EVENTS = ['meeting.rtms_started', 'webinar.rtms_started', 'session.rtms_started']
RTMS_STOP_EVENTS = ['meeting.rtms_stopped', 'webinar.rtms_stopped', 'session.rtms_stopped']
clients = {}
@rtms.onWebhookEvent
def handle_webhook(webhook):
event = webhook.get('event')
payload = webhook.get('payload', {})
stream_id = payload.get('rtms_stream_id')
if event in RTMS_STOP_EVENTS:
if stream_id in clients:
clients[stream_id].leave()
del clients[stream_id]
return
if event not in RTMS_EVENTS:
return
client = rtms.Client()
clients[stream_id] = client
@client.onTranscriptData
def on_transcript(data, size, timestamp, metadata):
text = data.decode('utf-8')
print(f'[{metadata.userName}]: {text}')
@client.onJoinConfirm
def on_join(reason):
print(f'Joined: {reason}')
@client.onLeave
def on_leave(reason):
print(f'Left: {reason}')
# SDK accepts both meeting_uuid and session_id transparently
client.join(payload)
# Main loop
if __name__ == '__main__':
print('Webhook server running...')
rtms.run()
Environment Variables Reference
# Required
ZM_RTMS_CLIENT=your_client_id
ZM_RTMS_SECRET=your_client_secret
# Optional
ZM_RTMS_PORT=8080 # Webhook server port
ZM_RTMS_PATH=/webhook # Webhook endpoint path
# Logging
ZM_RTMS_LOG_LEVEL=info # error, warn, info, debug, trace
ZM_RTMS_LOG_FORMAT=progressive # progressive or json
ZM_RTMS_LOG_ENABLED=true
Common Issues
| Issue | Solution |
|---|---|
| Segmentation fault | Upgrade to Node.js 20.3.0+ (24 LTS recommended) |
| Audio metadata missing userId | Use onActiveSpeakerEvent for speaker identification with mixed stream |
| Video params ignored | Call setVideoParams BEFORE setAudioParams |
Next Steps
- Manual WebSocket - Full protocol control without SDK
- AI Integration - Transcription and analysis patterns
- Media Types - All configuration options