366 lines
9.3 KiB
Markdown
366 lines
9.3 KiB
Markdown
# SDK Quickstart
|
|
|
|
The fastest way to receive RTMS media using the official `@zoom/rtms` SDK.
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
# Requires Node.js 20.3.0+ (24 LTS recommended)
|
|
npm install @zoom/rtms express
|
|
```
|
|
|
|
## Environment Setup
|
|
|
|
```bash
|
|
# .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.
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```bash
|
|
# 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](manual-websocket.md)** - Full protocol control without SDK
|
|
- **[AI Integration](ai-integration.md)** - Transcription and analysis patterns
|
|
- **[Media Types](../references/media-types.md)** - All configuration options
|