Setting the file. One moment.
Subchapter 134.10
examples/manual-websocket.mdMarkdown17 KBView on GitHub
Full RTMS protocol implementation without the SDK. Use this for:
RTMS requires two WebSocket connections:
const WebSocket = require('ws');
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json());
// Configuration
const CLIENT_ID = process.env.ZOOM_CLIENT_ID;
const
| msg_type | Name | Direction | Description |
|---|---|---|---|
| 1 | SIGNALING_HAND_SHAKE_REQ | Client -> Server | Initial handshake |
| 2 | SIGNALING_HAND_SHAKE_RESP | Server -> Client | Handshake response with media URL |
| 5 | EVENT_SUBSCRIPTION | Client -> Server | Subscribe to events |
| 6 | EVENT_UPDATE | Server -> Client | Event notification |
| 7 | CLIENT_READY_ACK | Client -> Server | Ready to receive media |
| 8 | STREAM_STATE_UPDATE | Server -> Client | Stream state changed |
| 9 | SESSION_STATE_UPDATE | Server -> Client | Session state changed |
| 12 | KEEP_ALIVE_REQ | Server -> Client | Heartbeat ping |
| 13 | KEEP_ALIVE_RESP | Client -> Server | Heartbeat pong |
| msg_type | Name | Direction | Description |
|---|---|---|---|
| 3 | DATA_HAND_SHAKE_REQ | Client -> Server | Media handshake with params |
| 4 | DATA_HAND_SHAKE_RESP | Server -> Client | Media handshake response |
| 12 | KEEP_ALIVE_REQ | Server -> Client | Heartbeat ping |
| 13 | KEEP_ALIVE_RESP | Client -> Server | Heartbeat pong |
| 14 | MEDIA_DATA_AUDIO | Server -> Client | Audio data |
| 15 | MEDIA_DATA_VIDEO | Server -> Client | Video data |
| 16 | MEDIA_DATA_SHARE | Server -> Client | Screen share data |
| 17 | MEDIA_DATA_TRANSCRIPT | Server -> Client | Transcript data |
| 18 | MEDIA_DATA_CHAT | Server -> Client | Chat message |
{
content_type: 2, // 1=RTP, 2=RAW_AUDIO
sample_rate: 1, // 0=8kHz, 1=16kHz, 2=32kHz, 3=48kHz
channel: 1, // 1=Mono, 2=Stereo (OPUS only)
codec: 1, // 1=L16, 2=G.711, 3=G.722, 4=OPUS
data_opt: 1, // 1=Mixed, 2=Multi-streams
send_rate: 20 // Chunk size in ms (multiple of 20)
}
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()
}));
}
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
}));
}PARTICIPANT_VIDEO_ON / PARTICIPANT_VIDEO_OFF events tell you which participants currently have subscribable camera streams.VIDEO_SINGLE_INDIVIDUAL_STREAM in the video media handshake and then send VIDEO_SUBSCRIPTION_REQ.STREAM_CLOSE_REQ / STREAM_CLOSE_RESP let the backend terminate a stream cleanly.PARTICIPANT_VIDEO_ON = 8PARTICIPANT_VIDEO_OFF = 9STREAM_CLOSE_REQ = 21STREAM_CLOSE_RESP = 22VIDEO_SUBSCRIPTION_REQ = 28VIDEO_SUBSCRIPTION_RESP = 29{
content_type: 3, // 3=RAW_VIDEO
codec: 7, // 5=JPG, 6=PNG, 7=H.264
resolution: 2, // 1=SD, 2=HD, 3=FHD, 4=QHD
fps: 25, // 1-30 (JPG/PNG max 5)
data_opt: 3 // 3=Single active speaker
}{
content_type: 3, // 3=RAW_VIDEO
codec: 5, // 5=JPG, 6=PNG, 7=H.264
resolution: 3, // 1=SD, 2=HD, 3=FHD, 4=QHD
fps: 1 // 1-30 (JPG/PNG max 1)
}{
content_type: 5, // 5=TEXT
src_language: 9, // 9=English
enable_lid: false // Fixed language, no auto-switch
}| Code | Name | Description |
|---|---|---|
| 0 | STATUS_OK | Success |
| 3 | STATUS_INVALID_SIGNATURE | Invalid signature |
| 8 | STATUS_DUPLICATE_SIGNAL_REQUEST | Duplicate signaling connection |
| 16 | STATUS_DUPLICATE_MEDIA_DATA_CONNECTION | Duplicate media connection |
| 40 | STATUS_INVALID_RTMS_SESSION_ID | Invalid RTMS session ID |
| 43 | STATUS_INVALID_MEDIA_TRANSCRIPT_SROUCE_LANGUAGE | Invalid transcript source language |
See Data Types for complete list.
// Implement exponential backoff for reconnection
let retryDelay = 1000;
ws.on('close', (code, reason) => {
console.log('Connection closed:', code, reason);
// Don't reconnect if intentionally closed
if (code === 1000) return;
setTimeout(() => {
reconnect();
}, retryDelay);
retryDelay = Math.min(retryDelay * 2, 30000);
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
// Connection will close, triggering reconnection
});Fill gaps with silence for continuous playback:
function handleAudioData(msg, streamId) {
const now = msg.timestamp;
const last = lastTimestamps.get(streamId) || now;
const gap = now - last;
// Fill gaps >= 500ms with silence
if (gap >= 500) {
const silentFrames = Math.floor(gap / 20);
console.log(`Filling ${silentFrames} silent frames`);
for (let i = 0; i < silentFrames; i++) {
const silentFrame = Buffer.alloc(640); // 20ms @ 16kHz mono
writeToFile(silentFrame);
}
}
lastTimestamps.set(streamId, now);
const audioBuffer = Buffer.from(msg.content, 'base64');
writeToFile(audioBuffer);
}