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/examples/sdk-quickstart.md
# 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