Files
2026-07-13 12:20:06 +08:00

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