10 KiB
10 KiB
WebSockets - Connection Management
Detailed guide for managing WebSocket connections to Zoom.
Connection Lifecycle
1. Generate access token (S2S OAuth)
↓
2. Open WebSocket connection with token
↓
3. Receive events in real-time
↓
4. Handle disconnects and reconnect
↓
5. Close connection when done
Authentication
WebSocket connections require a valid Server-to-Server OAuth access token.
Generate Access Token
const axios = require('axios');
async function getAccessToken(accountId, clientId, clientSecret) {
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
const response = await axios.post(
'https://zoom.us/oauth/token',
new URLSearchParams({
grant_type: 'account_credentials',
account_id: accountId
}),
{
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
return {
accessToken: response.data.access_token,
expiresIn: response.data.expires_in // Usually 3600 seconds (1 hour)
};
}
Token Refresh
Access tokens expire after 1 hour. Implement token refresh before expiration:
class ZoomWebSocketClient {
constructor(accountId, clientId, clientSecret, subscriptionId) {
this.accountId = accountId;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.subscriptionId = subscriptionId;
this.ws = null;
this.tokenExpiry = null;
}
async refreshTokenIfNeeded() {
const now = Date.now();
const bufferTime = 5 * 60 * 1000; // 5 minutes before expiry
if (!this.tokenExpiry || now >= this.tokenExpiry - bufferTime) {
const { accessToken, expiresIn } = await getAccessToken(
this.accountId, this.clientId, this.clientSecret
);
this.accessToken = accessToken;
this.tokenExpiry = now + (expiresIn * 1000);
// Reconnect with new token
if (this.ws) {
this.ws.close();
await this.connect();
}
}
}
async connect() {
await this.refreshTokenIfNeeded();
const wsUrl = `wss://ws.zoom.us/ws?subscriptionId=${this.subscriptionId}&access_token=${this.accessToken}`;
this.ws = new WebSocket(wsUrl);
// Set up event handlers...
}
}
Connection URL
wss://ws.zoom.us/ws?subscriptionId={SUBSCRIPTION_ID}&access_token={ACCESS_TOKEN}
| Parameter | Description |
|---|---|
subscriptionId |
Your WebSocket subscription ID from Marketplace |
access_token |
Valid S2S OAuth access token |
Connection Limits
| Limit | Value |
|---|---|
| Connections per subscription | 1 (opening new connection closes existing) |
| Connection timeout | Varies (implement keep-alive) |
| Message size | Check Zoom docs for current limits |
Keep-Alive / Heartbeat
Maintain connection with periodic pings:
class WebSocketManager {
constructor() {
this.ws = null;
this.pingInterval = null;
}
startHeartbeat() {
// Ping every 30 seconds
this.pingInterval = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.ping();
console.log('Ping sent');
}
}, 30000);
}
stopHeartbeat() {
if (this.pingInterval) {
clearInterval(this.pingInterval);
this.pingInterval = null;
}
}
connect(url) {
this.ws = new WebSocket(url);
this.ws.on('open', () => {
console.log('Connected');
this.startHeartbeat();
});
this.ws.on('pong', () => {
console.log('Pong received - connection alive');
});
this.ws.on('close', () => {
this.stopHeartbeat();
});
}
}
Reconnection Strategy
Implement exponential backoff for reconnection:
class ReconnectingWebSocket {
constructor(config) {
this.config = config;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.baseDelay = 1000; // 1 second
this.maxDelay = 30000; // 30 seconds
}
async connect() {
try {
const token = await getAccessToken(
this.config.accountId,
this.config.clientId,
this.config.clientSecret
);
const url = `wss://ws.zoom.us/ws?subscriptionId=${this.config.subscriptionId}&access_token=${token.accessToken}`;
this.ws = new WebSocket(url);
this.ws.on('open', () => {
console.log('Connected successfully');
this.reconnectAttempts = 0; // Reset on successful connection
});
this.ws.on('close', (code, reason) => {
console.log(`Disconnected: ${code} - ${reason}`);
this.scheduleReconnect();
});
this.ws.on('error', (error) => {
console.error('WebSocket error:', error.message);
});
this.ws.on('message', (data) => {
this.handleMessage(JSON.parse(data));
});
} catch (error) {
console.error('Connection failed:', error.message);
this.scheduleReconnect();
}
}
scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
return;
}
// Exponential backoff with jitter
const delay = Math.min(
this.baseDelay * Math.pow(2, this.reconnectAttempts) + Math.random() * 1000,
this.maxDelay
);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
}
handleMessage(event) {
// Override this method to handle events
console.log('Event:', event.event, event.payload);
}
close() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
}
Error Handling
Common Error Codes
| Code | Meaning | Action |
|---|---|---|
| 1000 | Normal closure | Clean shutdown |
| 1001 | Going away | Server shutting down, reconnect |
| 1006 | Abnormal closure | Network issue, reconnect |
| 1008 | Policy violation | Check token validity |
| 1011 | Internal error | Server error, retry later |
Error Handling Example
ws.on('close', (code, reason) => {
switch (code) {
case 1000:
console.log('Connection closed normally');
break;
case 1001:
case 1006:
console.log('Connection lost, reconnecting...');
scheduleReconnect();
break;
case 1008:
console.log('Auth error - refreshing token');
refreshTokenAndReconnect();
break;
default:
console.log(`Unexpected close: ${code} - ${reason}`);
scheduleReconnect();
}
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
// The 'close' event will follow, handle reconnection there
});
Complete Example
const WebSocket = require('ws');
const axios = require('axios');
class ZoomWebSocketClient {
constructor(config) {
this.config = config;
this.ws = null;
this.accessToken = null;
this.tokenExpiry = null;
this.pingInterval = null;
this.reconnectAttempts = 0;
this.handlers = new Map();
}
on(event, handler) {
this.handlers.set(event, handler);
}
async getAccessToken() {
const credentials = Buffer.from(
`${this.config.clientId}:${this.config.clientSecret}`
).toString('base64');
const response = await axios.post(
'https://zoom.us/oauth/token',
new URLSearchParams({
grant_type: 'account_credentials',
account_id: this.config.accountId
}),
{
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
this.accessToken = response.data.access_token;
this.tokenExpiry = Date.now() + (response.data.expires_in * 1000);
return this.accessToken;
}
async connect() {
await this.getAccessToken();
const url = `wss://ws.zoom.us/ws?subscriptionId=${this.config.subscriptionId}&access_token=${this.accessToken}`;
this.ws = new WebSocket(url);
this.ws.on('open', () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
this.startPing();
this.scheduleTokenRefresh();
});
this.ws.on('message', (data) => {
const event = JSON.parse(data);
const handler = this.handlers.get(event.event);
if (handler) {
handler(event.payload);
}
});
this.ws.on('close', (code, reason) => {
console.log(`Disconnected: ${code}`);
this.stopPing();
if (code !== 1000) {
this.reconnect();
}
});
this.ws.on('error', (error) => {
console.error('Error:', error.message);
});
}
startPing() {
this.pingInterval = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.ping();
}
}, 30000);
}
stopPing() {
if (this.pingInterval) {
clearInterval(this.pingInterval);
}
}
scheduleTokenRefresh() {
const refreshIn = this.tokenExpiry - Date.now() - 300000; // 5 min before expiry
setTimeout(() => this.refreshToken(), refreshIn);
}
async refreshToken() {
await this.getAccessToken();
// Close and reconnect with new token
this.ws?.close(1000);
await this.connect();
}
reconnect() {
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
this.reconnectAttempts++;
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(() => this.connect(), delay);
}
disconnect() {
this.stopPing();
this.ws?.close(1000);
}
}
// Usage
const client = new ZoomWebSocketClient({
accountId: process.env.ZOOM_ACCOUNT_ID,
clientId: process.env.ZOOM_CLIENT_ID,
clientSecret: process.env.ZOOM_CLIENT_SECRET,
subscriptionId: process.env.ZOOM_SUBSCRIPTION_ID
});
client.on('meeting.started', (payload) => {
console.log(`Meeting started: ${payload.object.topic}`);
});
client.on('meeting.ended', (payload) => {
console.log(`Meeting ended: ${payload.object.uuid}`);
});
client.on('meeting.participant_joined', (payload) => {
console.log(`Participant joined: ${payload.object.participant.user_name}`);
});
client.connect();
Resources
- WebSockets docs: https://developers.zoom.us/docs/api/websockets/
- S2S OAuth guide: https://developers.zoom.us/docs/internal-apps/s2s-oauth/