Zoom Rtms
OpenAIOfficialCreated by ZoomDistributed by OpenAI
- Skill
- Tool integration
- Automation and integrations
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The zoom-rtms skill provides the necessary protocols and architectural guidance for integrating Zoom Realtime Media Streams. It covers the WebSocket-based connection lifecycle, including signaling and media handshakes, heartbeat management, and reconnection strategies. The skill supports processing various media types such as audio, video, screen sharing, transcripts, and chat, while providing reference data for message types and event enums.
What it does
- Establishes WebSocket connections for signaling and media streams.
- Handles media handshake requests for audio, video, screen share, transcript, and chat data.
- Provides reference tables for RTMS message types and event enums.
- Defines mandatory heartbeat and reconnection logic for stream stability.
- Supports configuration for transcript language identification.
- Outlines the process for extracting stream IDs and generating HMAC-SHA256 signatures.
When to use it
- When you need to ingest raw meeting or contact center media into a backend pipeline.
- When building services for transcription, AI analysis, or recording of Zoom sessions.
- When you need to subscribe to specific meeting events or participant media streams.
- When you require programmatic access to media data without a visible participant bot.
Advantages
- Provides a structured approach to managing the complex WebSocket lifecycle.
- Includes comprehensive references for message types and event handling.
- Offers clear guidance on signature generation and security requirements.
- Enables granular control over media subscription and transcript settings.
Usage examples
- “How do I initiate a connection to an RTMS stream?”
- The agent will guide you through receiving the rtms_started webhook, generating an HMAC-SHA256 signature, and performing the signaling and media handshakes.
- “What is the correct way to handle heartbeats in RTMS?”
- The agent will explain that you must respond to msg_type 12 (Keep Alive Request) with msg_type 13 (Keep Alive Response) to maintain the connection.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/openai/zoom-rtms/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip zoom-rtms.zip -d .agents/skills
Documentation by Zoom
Zoom Realtime Media Streams
Use this skill when the integration needs real-time meeting or contact-center media as a backend stream. If the workflow needs a visible participant bot, compare against build-zoom-bot and zoom-meeting-sdk-linux before choosing RTMS.
Workflow
Confirm the source surface: Meetings RTMS, Contact Center voice media, or another documented RTMS stream.
Decide whether RTMS is sufficient or whether a Meeting SDK bot is required for participant identity, meeting controls, or UI-visible behavior.
Implement the WebSocket lifecycle first: event subscription, connection validation, media start, heartbeat, reconnect, and shutdown.
Process media types intentionally: audio, video, screen share, chat, live transcript, and metadata have different payload and timing constraints.
Design downstream pipelines for buffering, transcription, AI analysis, recording, or tool invocation.
Debug by isolating app setup, webhook/event delivery, stream authorization, network connectivity, and media payload decoding.
References
Full preserved guide: references/full-guide.md
Connection architecture: concepts/connection-architecture.md
Lifecycle flow: concepts/lifecycle-flow.md
Media types: references/media-types.md
Data types: references/data-types.md
RTMS bot example: examples/rtms-bot.md
Common issues: troubleshooting/common-issues.md
Files
- ATTRIBUTION.mdAdded by AutoGineer860 B
- LICENSELicence file added by AutoGineer1.1 KiB
- RUNBOOK.md2.6 KiB
- SKILL.md1.6 KiB
- agents/openai.yaml140 B
- concepts/connection-architecture.md9.1 KiB
- concepts/lifecycle-flow.md13.4 KiB
- examples/ai-integration.md11.3 KiB
- examples/manual-websocket.md16.8 KiB
- examples/rtms-bot.md25.9 KiB
- examples/sdk-quickstart.md9.3 KiB
- references/connection.md8.0 KiB
- references/data-types.md13.7 KiB
- references/environment-variables.md1.3 KiB
- references/full-guide.md22.9 KiB
- references/media-types.md6.0 KiB
- references/quickstart.md6.6 KiB
- references/webhooks.md7.4 KiB
- troubleshooting/common-issues.md10.4 KiB
examples/sdk-quickstart.md
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 expressEnvironment Setup
# .env
ZM_RTMS_CLIENT=your_client_id
ZM_RTMS_SECRET=your_client_secretMulti-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=trueCommon 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
Requirements and compatibility
Written for: OpenAI Codex
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Zoom
- Publisher
- OpenAI (Official)
- Distributed by
- OpenAI
- Source
- openai/plugins · plugins/zoom/skills/rtms · commit d420d13
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from plugins/zoom/LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Zoom or OpenAI.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download zoom-rtms.zip