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
references/connection.md
RTMS - Connection
WebSocket connection protocol details.
Connection Flow
1. Receive meeting/webinar/session.rtms_started webhook
↓
2. Extract server_urls, stream_id, and meeting_uuid or session_id
↓
3. Generate signature (HMAC-SHA256) using meeting_uuid or session_id
↓
4. Connect to signaling WebSocket
↓
5. Send handshake request (msg_type 1)
↓
6. Receive handshake response (msg_type 2) with media server URL
↓
7. Connect to media WebSocket(s)
↓
8. Send media handshake (msg_type 3)
↓
9. Receive media handshake response (msg_type 4)
↓
10. Send ready to receive (msg_type 7)
↓
11. Receive media data (msg_type 14-18)
↓
12. Respond to heartbeats (msg_type 12 → 13)
↓
13. Optionally react to `PARTICIPANT_VIDEO_ON/OFF`, send `VIDEO_SUBSCRIPTION_REQ`, or gracefully terminate with `STREAM_CLOSE_REQ`Signature Generation
const crypto = require('crypto');
// For meetings and webinars: use meeting_uuid
// For Video SDK: use session_id
// Webinars still use meeting_uuid (NOT webinar_uuid)
function generateSignature(clientId, idValue, streamId, clientSecret) {
const message = `${clientId},${idValue},${streamId}`;
return crypto.createHmac('sha256', clientSecret).update(message).digest('hex');
}
// Extract the correct ID from any product's webhook payload
const idValue = payload.meeting_uuid || payload.session_id;Signaling Message Types
| msg_type | Name | Direction | Description |
|---|---|---|---|
| 1 | Handshake Request | Client → Server | Initiate connection |
| 2 | Handshake Response | Server → Client | Returns media server URL |
| 3 | Media Handshake Request | Client → Server | Request specific media types |
| 4 | Media Handshake Response | Server → Client | Confirms media subscription |
| 7 | Ready to Receive | Client → Server | Signal ready for data |
| 12 | Keep Alive Request | Server → Client | Heartbeat ping |
| 13 | Keep Alive Response | Client → Server | Heartbeat pong |
Media Message Types
| msg_type | Media Type |
|---|---|
| 14 | Audio |
| 15 | Video |
| 16 | Screen Share |
| 17 | Transcript |
| 18 | Chat |
Critical Gotchas
1. Only ONE Connection Per Stream!
// WRONG - Connecting twice kicks out first connection
connectToRTMS(serverUrl, streamId); // Connection 1
connectToRTMS(serverUrl, streamId); // Connection 2 - kicks out Connection 1!
// CORRECT - Only connect once
if (!activeConnections.has(streamId)) {
connectToRTMS(serverUrl, streamId);
activeConnections.set(streamId, ws);
}2. Heartbeat is MANDATORY
When you receive msg_type 12, you MUST respond with msg_type 13:
ws.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.msg_type === 12) { // Keep Alive Request
ws.send(JSON.stringify({
msg_type: 13, // Keep Alive Response
timestamp: msg.timestamp
}));
}
});3. Reconnection is YOUR Responsibility
RTMS does NOT auto-reconnect. Implement your own retry logic:
| Server Type | Timeout |
|---|---|
| Media Server | 65 seconds keep-alive tolerance before timeout |
| Signaling Server | 60 seconds to reconnect |
ws.on('close', () => {
// Implement exponential backoff
setTimeout(() => reconnect(), retryDelay);
retryDelay = Math.min(retryDelay * 2, 30000);
});Transcript LID Control
The transcript media handshake now supports explicit Language Identification control.
mediaWs.send(JSON.stringify({
msg_type: 3,
protocol_version: 1,
meeting_uuid: idValue,
rtms_stream_id: streamId,
signature,
media_type: 8, // TRANSCRIPT
media_params: {
transcript: {
content_type: 5, // TEXT
src_language: 9, // English
enable_lid: false // Lock to src_language instead of auto-switching
}
}
}));Use enable_lid: false when:
the meeting should stay on a known language
language-switching is undesirable
you want more predictable downstream transcript processing
Single Individual Video Subscription Flow
RTMS now supports subscribing to one participant camera stream at a time.
Open a video media socket with
data_opt = VIDEO_SINGLE_INDIVIDUAL_STREAMSubscribe to
PARTICIPANT_VIDEO_ONandPARTICIPANT_VIDEO_OFFWhen an event arrives, choose the
user_idyou wantSend
VIDEO_SUBSCRIPTION_REQon the signaling socketWait for
VIDEO_SUBSCRIPTION_RESPExpect the newest successful subscription to replace the previous participant stream
// Signaling socket: subscribe to control-plane events
signalingWs.send(JSON.stringify({
msg_type: 5, // EVENT_SUBSCRIPTION
events: [
{ event_type: 8, subscribe: true }, // PARTICIPANT_VIDEO_ON
{ event_type: 9, subscribe: true } // PARTICIPANT_VIDEO_OFF
]
}));
// Signaling socket: select a participant stream
signalingWs.send(JSON.stringify({
msg_type: 28, // VIDEO_SUBSCRIPTION_REQ
user_id: selectedUserId,
subscribe: true,
timestamp: Date.now()
}));The March 2026 changelog did not publish the numeric values for the new message types. Use the protocol definitions before hard-coding them.
Graceful Stream Closure
The backend can now request clean shutdown over the signaling socket:
signalingWs.send(JSON.stringify({
msg_type: 21, // STREAM_CLOSE_REQ
rtms_stream_id: streamId
}));Expect:
STREAM_CLOSE_RESPthen normal connection shutdown / cleanup
Use this when your app wants deterministic teardown instead of waiting for a stop webhook or socket failure.
Split vs Unified Mode
| Mode | Description | Best For |
|---|---|---|
| Split | One connection per media type | Most use cases. Media server supports multiple connections with different media types |
| Unified | One connection for all media | Real-time audio+video streaming/muxing where sync matters |
Low-Level Connection Example
const WebSocket = require('ws');
const crypto = require('crypto');
async function connectRTMS(webhookPayload) {
const { server_urls, rtms_stream_id } = webhookPayload;
// meeting_uuid for meetings/webinars, session_id for Video SDK
const idValue = webhookPayload.meeting_uuid || webhookPayload.session_id;
// Generate signature
const signature = crypto
.createHmac('sha256', process.env.ZOOM_CLIENT_SECRET)
.update(`${process.env.ZOOM_CLIENT_ID},${idValue},${rtms_stream_id}`)
.digest('hex');
// Connect to signaling server
const signalingWs = new WebSocket(server_urls, {
headers: {
'X-Zoom-RTMS-Stream-Id': rtms_stream_id,
'X-Zoom-RTMS-Signature': signature
}
});
signalingWs.on('open', () => {
// Send handshake request
signalingWs.send(JSON.stringify({
msg_type: 1,
protocol_version: 1,
client_id: process.env.ZOOM_CLIENT_ID,
meeting_uuid: idValue, // Works for both meeting_uuid and session_id
stream_id: rtms_stream_id,
signature: signature,
media_type: 9 // AUDIO(1) | TRANSCRIPT(8)
}));
});
signalingWs.on('message', (data) => {
const msg = JSON.parse(data);
switch (msg.msg_type) {
case 2: // Handshake response
// Connect to media server from msg.media_server_url
connectMediaServer(msg.media_server_url);
break;
case 12: // Keep alive request
signalingWs.send(JSON.stringify({ msg_type: 13, timestamp: msg.timestamp }));
break;
}
});
signalingWs.on('error', (error) => {
console.error('Signaling error:', error);
});
signalingWs.on('close', (code, reason) => {
console.log('Signaling closed:', code, reason);
// Implement reconnection logic
});
}Resources
RTMS_CONNECTION_FLOW.md: https://github.com/zoom/rtms-samples/blob/main/RTMS_CONNECTION_FLOW.md
ARCHITECTURE.md: https://github.com/zoom/rtms-samples/blob/main/ARCHITECTURE.md
TROUBLESHOOTING.md: https://github.com/zoom/rtms-samples/blob/main/TROUBLESHOOTING.md
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