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
concepts/lifecycle-flow.md
RTMS Lifecycle Flow
Complete flow from meeting/webinar/session start to media streaming.
High-Level Flow
┌─────────────────────────────┐
│ Meeting/Webinar/Session │
│ Starts │
└────────────┬────────────────┘
│
▼
┌─────────────────────────────┐
│ Zoom sends webhook event │
│ meeting.rtms_started OR │
│ webinar.rtms_started OR │
│ session.rtms_started │
└────────────┬────────────────┘
│
▼
┌──────────────────┐
│ Your server │
│ receives │
│ webhook │
│ │
│ RESPOND 200 │
│ IMMEDIATELY! │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Connect to │
│ Signaling WS │
│ │
│ Send handshake │
│ (msg_type: 1) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Receive │
│ handshake resp │
│ (msg_type: 2) │
│ │
│ Extract media │
│ server URL │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Connect to │
│ Media WS │
│ │
│ Send handshake │
│ (msg_type: 3) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Receive media │
│ handshake resp │
│ (msg_type: 4) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Send Client │
│ Ready to │
│ Signaling │
│ (msg_type: 7) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Receive media │
│ data: │
│ - Audio (14) │
│ - Video (15) │
│ - Share (16) │
│ - Transcript(17)│
│ - Chat (18) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Respond to │
│ heartbeats │
│ (12 -> 13) │
└────────┬─────────┘
│
▼
┌─────────────────────────────┐
│ Optional control-plane │
│ actions during stream │
│ - EVENT_SUBSCRIPTION │
│ - VIDEO_SUBSCRIPTION_REQ │
│ - STREAM_CLOSE_REQ │
└────────────┬────────────────┘
│
▼
┌─────────────────────────────┐
│ meeting/webinar/session │
│ .rtms_stopped │
│ │
│ Close sockets │
│ Cleanup │
└─────────────────────────────┘Detailed Steps
Step 1: Receive Webhook
When RTMS starts, Zoom sends a webhook. The event name and payload differ by product:
Meeting RTMS:
{
"event": "meeting.rtms_started",
"payload": {
"account_id": "abc123",
"object": {
"meeting_id": "123456789",
"meeting_uuid": "AbC123...",
"host_id": "user123",
"rtms_stream_id": "stream123==",
"server_urls": "wss://rtms-sjc1.zoom.us/...",
"signature": "pre_computed_signature"
}
}
}Webinar RTMS:
{
"event": "webinar.rtms_started",
"payload": {
"account_id": "abc123",
"object": {
"meeting_id": "123456789",
"meeting_uuid": "AbC123...",
"host_id": "user123",
"rtms_stream_id": "stream123==",
"server_urls": "wss://rtms-sjc1.zoom.us/...",
"signature": "pre_computed_signature"
}
}
}Note: Webinar payloads use
meeting_uuid, NOTwebinar_uuid.
Video SDK RTMS:
{
"event": "session.rtms_started",
"payload": {
"account_id": "abc123",
"object": {
"session_id": "SessionABC...",
"rtms_stream_id": "stream123==",
"server_urls": "wss://rtms-sjc1.zoom.us/...",
"signature": "pre_computed_signature"
}
}
}Note: Video SDK payloads use
session_idinstead ofmeeting_uuid.
Product Differences
| Aspect | Meetings | Webinars | Video SDK |
|---|---|---|---|
| Webhook event | meeting.rtms_started | webinar.rtms_started | session.rtms_started |
| Payload ID field | meeting_uuid | meeting_uuid (same!) | session_id |
| App type | General App (OAuth) | General App (OAuth) | Video SDK App (SDK Key/Secret) |
| Participants | All participants | Panelists have full streams; attendees may not | All participants |
| Protocol after connect | Identical | Identical | Identical |
CRITICAL: Respond with HTTP 200 IMMEDIATELY before any processing!
const RTMS_EVENTS = ['meeting.rtms_started', 'webinar.rtms_started', 'session.rtms_started'];
app.post('/webhook', (req, res) => {
res.status(200).send(); // FIRST!
const { event, payload } = req.body;
if (RTMS_EVENTS.includes(event)) {
handleRTMSStarted(payload);
}
});Why? If you delay, Zoom retries the webhook. The retry creates a second connection, which kicks out your first connection.
Step 2: Connect to Signaling WebSocket
const signalingWs = new WebSocket(payload.server_urls);
// Use meeting_uuid for meetings/webinars, session_id for Video SDK
const idValue = payload.meeting_uuid || payload.session_id;
signalingWs.on('open', () => {
const signature = generateSignature(
CLIENT_ID,
idValue,
payload.rtms_stream_id,
CLIENT_SECRET
);
signalingWs.send(JSON.stringify({
msg_type: 1, // Handshake request
protocol_version: 1,
meeting_uuid: idValue,
rtms_stream_id: payload.rtms_stream_id,
signature: signature,
media_type: 9 // Audio(1) + Transcript(8)
}));
});Step 3: Handle Signaling Response
signalingWs.on('message', (data) => {
const msg = JSON.parse(data);
switch (msg.msg_type) {
case 2: // Handshake response
if (msg.status_code === 0) {
// Extract media server URL
const mediaUrl = msg.media_server.server_urls.all;
connectToMediaServer(mediaUrl);
} else {
console.error('Handshake failed:', msg.status_code);
}
break;
case 12: // Keep alive request
signalingWs.send(JSON.stringify({
msg_type: 13,
timestamp: msg.timestamp
}));
break;
}
});Step 4: Connect to Media WebSocket
function connectToMediaServer(mediaUrl) {
const mediaWs = new WebSocket(mediaUrl);
mediaWs.on('open', () => {
mediaWs.send(JSON.stringify({
msg_type: 3, // Media handshake request
protocol_version: 1,
meeting_uuid: idValue, // meeting_uuid or session_id
rtms_stream_id: streamId,
signature: signature,
media_type: 9, // Audio + Transcript
payload_encryption: false,
media_params: {
audio: {
content_type: 2, // RAW_AUDIO
sample_rate: 1, // 16kHz
channel: 1, // Mono
codec: 1, // L16 (PCM)
data_opt: 1, // Mixed stream
send_rate: 20 // 20ms chunks
},
transcript: {
content_type: 5, // TEXT
src_language: 9, // English
enable_lid: false // Fixed language, no auto-switch
}
}
}));
});
}Step 5: Start Streaming
After media handshake succeeds, tell signaling you're ready:
mediaWs.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.msg_type === 4 && msg.status_code === 0) {
// Media handshake success - tell signaling we're ready
signalingWs.send(JSON.stringify({
msg_type: 7, // Client ready
rtms_stream_id: streamId
}));
}
});Step 6: Receive Media Data
mediaWs.on('message', (data) => {
const msg = JSON.parse(data);
switch (msg.msg_type) {
case 14: // Audio
const audioBuffer = Buffer.from(msg.content, 'base64');
processAudio(audioBuffer, msg.user_name, msg.timestamp);
break;
case 15: // Video
const videoBuffer = Buffer.from(msg.content, 'base64');
processVideo(videoBuffer, msg.user_name, msg.timestamp);
break;
case 16: // Screen share
const shareBuffer = Buffer.from(msg.content, 'base64');
processScreenShare(shareBuffer, msg.user_name, msg.timestamp);
break;
case 17: // Transcript
console.log(`${msg.user_name}: ${msg.content}`);
break;
case 18: // Chat
console.log(`[Chat] ${msg.user_name}: ${msg.content}`);
break;
case 12: // Keep alive
mediaWs.send(JSON.stringify({
msg_type: 13,
timestamp: msg.timestamp
}));
break;
}
});Step 6A: Track Available Participant Video Streams
When using the new single-individual-video mode, the signaling socket tells you whose camera is currently available.
const activeVideoUsers = new Set();
function handleEventUpdate(msg) {
const eventType = msg.event?.event_type;
const participants = msg.event?.participants || [];
if (eventType === 8) { // PARTICIPANT_VIDEO_ON
for (const participant of participants) activeVideoUsers.add(participant.user_id);
}
if (eventType === 9) { // PARTICIPANT_VIDEO_OFF
for (const participant of participants) activeVideoUsers.delete(participant.user_id);
}
}Use these events as the control-plane signal for which participant video streams are currently subscribable.
Step 6B: Select One Participant Video Stream
function subscribeToParticipantVideo(streamId, userId) {
const signalingWs = signalingConnections.get(streamId);
if (!signalingWs) return;
signalingWs.send(JSON.stringify({
msg_type: 28, // VIDEO_SUBSCRIPTION_REQ
user_id: userId,
subscribe: true,
timestamp: Date.now()
}));
}Important constraint:
only one participant stream can be active at a time
the newest successful subscription replaces the previous selection
Step 7: Handle Session End
const RTMS_STOP_EVENTS = ['meeting.rtms_stopped', 'webinar.rtms_stopped', 'session.rtms_stopped'];
// Via webhook
app.post('/webhook', (req, res) => {
res.status(200).send();
const { event, payload } = req.body;
if (RTMS_STOP_EVENTS.includes(event)) {
const streamId = payload.rtms_stream_id;
// Close connections
signalingConnections.get(streamId)?.close();
mediaConnections.get(streamId)?.close();
// Cleanup
signalingConnections.delete(streamId);
mediaConnections.delete(streamId);
}
});
// Also handle WebSocket close events
signalingWs.on('close', (code, reason) => {
console.log('Signaling closed:', code, reason);
// Implement reconnection if needed
});Optional: Client-Initiated Graceful Close
The backend can now ask RTMS to terminate the stream cleanly:
function closeStream(streamId) {
const signalingWs = signalingConnections.get(streamId);
if (!signalingWs) return;
signalingWs.send(JSON.stringify({
msg_type: 21, // STREAM_CLOSE_REQ
rtms_stream_id: streamId
}));
}Expect a STREAM_CLOSE_RESP followed by normal socket teardown.
Session Tracking
CRITICAL: Track active sessions to prevent duplicate connections!
const activeSessions = new Map();
function handleRTMSStarted(payload) {
const streamId = payload.rtms_stream_id;
// Check for existing connection
if (activeSessions.has(streamId)) {
console.log('Already connected to this stream, ignoring');
return;
}
// Mark as active (meeting_uuid for meetings/webinars, session_id for Video SDK)
activeSessions.set(streamId, {
startTime: Date.now(),
idValue: payload.meeting_uuid || payload.session_id
});
// Connect
connectToRTMS(payload);
}
function handleRTMSStopped(payload) {
const streamId = payload.rtms_stream_id;
activeSessions.delete(streamId);
// ... cleanup
}Error Handling
// SDK state management (from Arlo sample)
try {
client.join(payload);
} catch (error) {
if (error.message?.includes('Invalid status')) {
console.warn('SDK in invalid state, waiting to retry...');
setTimeout(() => {
handleRTMSStarted(payload);
}, 2000);
}
}Next Steps
SDK Quickstart - SDK handles all this automatically
Manual WebSocket - Full implementation code
Common Issues - Debugging connection problems
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