Generated banner for Zoom Video SDK Web
Generated logo for Zoom Video SDK Web

Zoom Video SDK Web

OpenAIOfficialCreated by ZoomDistributed by OpenAI

  • Skill
  • Tool integration
  • Software development
  • MIT
  • No issues found

From the author

Zoom Video SDK for Web - JavaScript/TypeScript integration for browser-based video sessions, real-time communication, screen sharing, recording, and live transcription
— the description by Zoom

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The Zoom Video SDK for Web provides a robust JavaScript/TypeScript library for building custom, browser-based video communication experiences. Unlike standard Zoom meetings, this SDK allows developers to create fully branded, custom user interfaces for video sessions, screen sharing, live transcription, and collaborative features like whiteboarding. It follows a strict event-driven architecture and requires careful management of the session lifecycle, particularly regarding media stream initialization and participant event handling.

What it does

  • Enables real-time audio and video communication in web browsers.
  • Supports screen sharing and collaborative whiteboarding.
  • Provides tools for cloud recording and live streaming to RTMP endpoints.
  • Includes features for in-session chat and custom command messaging.
  • Facilitates live transcription and breakout room management.
  • Offers granular control over media devices and video quality settings.

When to use it

  • When building a custom-branded video conferencing application.
  • When you need full control over the video UI and participant layout.
  • When integrating real-time communication into existing web platforms.
  • When requiring advanced features like custom video frame processing or live captions.

Advantages

  • Full control over the user interface and user experience.
  • Supports high-definition video and WebRTC for direct peer-to-peer streaming.
  • Comprehensive feature set including chat, recording, and transcription.
  • Well-documented lifecycle patterns for reliable session management.

Usage examples

“How do I join a video session using the Zoom Video SDK?”
The user receives the 5-step lifecycle pattern: create client, initialize, join, get media stream, and start media, with specific warnings to only call getMediaStream() after joining.
“How can I display a remote participant's video?”
The user is guided to use the peer-video-state-change event to trigger attachVideo() and append the resulting video element to the DOM.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/openai/zoom-video-sdk-web/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip zoom-video-sdk-web.zip -d .agents/skills

Documentation by Zoom

Zoom Video SDK - Web Development

Expert guidance for developing with the Zoom Video SDK on Web. This SDK enables custom video applications in the browser with real-time video/audio, screen sharing, cloud recording, live streaming, chat, and live transcription.

This skill is for custom video sessions, not embedded Zoom meetings. If the user wants a custom UI for a real Zoom meeting, route to ../../meeting-sdk/web/component-view/SKILL.md.

Official Documentation: https://developers.zoom.us/docs/video-sdk/web/ API Reference: https://marketplacefront.zoom.us/sdk/custom/web/modules.html Sample Repository: https://github.com/zoom/videosdk-web-sample

New to Video SDK? Follow this path:

  1. SDK Architecture Pattern - Universal 3-step pattern for ANY feature

  2. Session Join Pattern - Complete working code to join a session

  3. Video Rendering - Display video with attachVideo()

  4. Event Handling - Required events for video/audio

Reference:

  • Singleton Hierarchy - 4-level SDK navigation map

  • API Reference - Methods, events, error codes

  • SKILL.md - Complete documentation navigation

  • ../../probe-sdk/SKILL.md - Optional browser/device/network readiness diagnostics before join

Having issues?

  • Video not showing → Video Rendering (use attachVideo, not renderVideo)

  • getMediaStream() returns undefined → Call AFTER join() completes

  • Quick diagnostics → Common Issues

SDK Overview

The Zoom Video SDK for Web is a JavaScript library that provides:

  • Session Management: Join/leave video SDK sessions

  • Video/Audio: Start/stop camera and microphone

  • Screen Sharing: Share screens or browser tabs

  • Cloud Recording: Record sessions to Zoom cloud

  • Live Streaming: Stream to RTMP endpoints

  • Chat: In-session messaging

  • Command Channel: Custom command messaging

  • Live Transcription: Real-time speech-to-text

  • Subsessions: Breakout room support

  • Whiteboard: Collaborative whiteboard features

  • Virtual Background: Blur or custom image backgrounds

Prerequisites

System Requirements
  • Modern Browser: Chrome 80+, Firefox 75+, Safari 14+, Edge 80+

  • Video SDK Credentials: SDK Key and Secret from Marketplace

  • JWT Token: Server-side generated signature

Browser Feature Requirements
// Check browser compatibility before init
const compatibility = ZoomVideo.checkSystemRequirements();
console.log('Audio:', compatibility.audio);
console.log('Video:', compatibility.video);
console.log('Screen:', compatibility.screen);

// Check feature support
const features = ZoomVideo.checkFeatureRequirements();
console.log('Supported:', features.supportFeatures);
console.log('Unsupported:', features.unSupportFeatures);

Use Probe SDK as a readiness gate before client.join(...) when you need to reduce failed starts:

  1. Run diagnostics with ../../probe-sdk/SKILL.md.

  2. Evaluate policy (allow, warn, block).

  3. Start Video SDK join only when policy allows.

Cross-skill flow: ../../general/use-cases/probe-sdk-preflight-readiness-gate.md

Installation

npm install @zoom/videosdk
import ZoomVideo from '@zoom/videosdk';

Note: Some networks/ad blockers can block source.zoom.us. If you see flaky loads, first try allowlisting the domain in your environment. If needed, consider a fallback (mirror/self-host) only if it's permitted for your use case and you can keep versions in sync.

# Download SDK locally
curl "https://source.zoom.us/videosdk/zoom-video-2.3.12.min.js" -o public/js/zoom-video-sdk.min.js
<!-- Use local copy instead of CDN -->
<script src="js/zoom-video-sdk.min.js"></script>
// CDN exports as WebVideoSDK, NOT ZoomVideo
const ZoomVideo = WebVideoSDK.default;

Quick Start

import ZoomVideo from '@zoom/videosdk';

// 1. Create client (singleton - returns same instance)
const client = ZoomVideo.createClient();

// 2. Initialize SDK
await client.init('en-US', 'Global', { patchJsMedia: true });

// 3. Join session
await client.join(topic, signature, userName, password);

// 4. CRITICAL: Get stream AFTER join
const stream = client.getMediaStream();

// 5. Start media
await stream.startVideo();
await stream.startAudio();

// 6. Attach video to DOM
const videoElement = await stream.attachVideo(userId, VideoQuality.Video_360P);
document.getElementById('video-container').appendChild(videoElement);

SDK Lifecycle (CRITICAL ORDER)

The SDK has a strict lifecycle. Violating it causes silent failures.

1. Create client:     client = ZoomVideo.createClient()
2. Initialize:        await client.init('en-US', 'Global', options)
3. Join session:      await client.join(topic, signature, userName, password)
4. Get stream:        stream = client.getMediaStream()  ← ONLY AFTER JOIN
5. Start media:       await stream.startVideo() / await stream.startAudio()

Common Mistake:

// WRONG: Getting stream before joining
const stream = client.getMediaStream();  // Returns undefined!
await client.join(...);

// CORRECT: Get stream after joining
await client.join(...);
const stream = client.getMediaStream();  // Works!

Critical Gotchas and Best Practices

getMediaStream() ONLY Works After join()

The #1 issue that causes video/audio to fail:

// WRONG
const stream = client.getMediaStream();  // undefined!
await client.join(...);

// CORRECT
await client.join(...);
const stream = client.getMediaStream();  // Works
Use attachVideo() NOT renderVideo()

renderVideo() is deprecated. Use attachVideo() which returns a VideoPlayer element:

import { VideoQuality } from '@zoom/videosdk';

// CORRECT: attachVideo returns element to append
const videoElement = await stream.attachVideo(userId, VideoQuality.Video_360P);
document.getElementById('video-container').appendChild(videoElement);

// WRONG: renderVideo is deprecated
await stream.renderVideo(canvas, userId, ...);  // Don't use!
Video Rendering is Event-Driven (CRITICAL)

You MUST listen for events to properly render participant videos:

// When another participant's video state changes
client.on('peer-video-state-change', async (payload) => {
  const { action, userId } = payload;

  if (action === 'Start') {
    // Participant turned on video - attach it
    const element = await stream.attachVideo(userId, VideoQuality.Video_360P);
    container.appendChild(element);
  } else if (action === 'Stop') {
    // Participant turned off video - detach it
    await stream.detachVideo(userId);
  }
});

// When participants join/leave
client.on('user-added', (payload) => {
  // New participant joined - check if their video is on
  const users = client.getAllUser();
  // Render videos for users with bVideoOn === true
});

client.on('user-removed', (payload) => {
  // Participant left - clean up their video element
  stream.detachVideo(payload[0].userId);
});
Peer Video on Mid-Session Join

Existing participants' videos won't auto-render when you join mid-session.

// After joining, render existing participants' videos
const renderExistingVideos = async () => {
  await new Promise(resolve => setTimeout(resolve, 500));

  const users = client.getAllUser();
  const currentUserId = client.getCurrentUserInfo().userId;

  for (const user of users) {
    if (user.bVideoOn && user.userId !== currentUserId) {
      const element = await stream.attachVideo(user.userId, VideoQuality.Video_360P);
      document.getElementById(`video-${user.userId}`).appendChild(element);
    }
  }
};
CDN Race Condition with ES Modules

When using <script type="module"> with CDN, the SDK may not be loaded yet:

function waitForSDK(timeout = 10000) {
  return new Promise((resolve, reject) => {
    if (typeof WebVideoSDK !== 'undefined') {
      resolve();
      return;
    }
    const start = Date.now();
    const check = setInterval(() => {
      if (typeof WebVideoSDK !== 'undefined') {
        clearInterval(check);
        resolve();
      } else if (Date.now() - start > timeout) {
        clearInterval(check);
        reject(new Error('SDK failed to load'));
      }
    }, 100);
  });
}

await waitForSDK();
const ZoomVideo = WebVideoSDK.default;
SharedArrayBuffer for HD Video

For optimal performance and HD video, configure these headers on your server:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Note: As of v1.11.2, SharedArrayBuffer is elective (not strictly required).

Check HD Capability Before Enabling
const stream = client.getMediaStream();

// Check if 720p is supported
const hdSupported = stream.isSupportHDVideo();

// Get maximum video quality
const maxQuality = stream.getVideoMaxQuality();
// 0=90P, 1=180P, 2=360P, 3=720P, 4=1080P

// Start video with HD
if (hdSupported) {
  await stream.startVideo({ hd: true });
}
Screen Share Rendering Mode Check
const stream = client.getMediaStream();

// Check which element type to use
if (stream.isStartShareScreenWithVideoElement()) {
  // Use video element
  const video = document.getElementById('share-video');
  await stream.startShareScreen(video as unknown as HTMLCanvasElement);
} else {
  // Use canvas element
  const canvas = document.getElementById('share-canvas');
  await stream.startShareScreen(canvas);
}

Key Features

Video Quality Enum
import { VideoQuality } from '@zoom/videosdk';

VideoQuality.Video_90P   // 0
VideoQuality.Video_180P  // 1
VideoQuality.Video_360P  // 2 (recommended for most cases)
VideoQuality.Video_720P  // 3
VideoQuality.Video_1080P // 4
Virtual Backgrounds
const stream = client.getMediaStream();

// Always check support first
if (stream.isSupportVirtualBackground()) {
  // Blur background
  await stream.updateVirtualBackgroundImage('blur');

  // Custom image background
  await stream.updateVirtualBackgroundImage('https://example.com/bg.jpg');

  // Remove virtual background
  await stream.updateVirtualBackgroundImage(undefined);
}
Video Processor (Custom Effects)

The VideoProcessor class allows you to intercept and modify video frames:

// video-processor-worker.js
class MyVideoProcessor extends VideoProcessor {
  processFrame(input, output) {
    const ctx = output.getContext('2d');
    ctx.drawImage(input, 0, 0);

    // Add overlay
    ctx.fillStyle = 'white';
    ctx.font = '24px Arial';
    ctx.fillText('Live', 20, 40);

    return true;
  }
}
WebRTC Mode

Enable WebRTC mode for direct peer-to-peer streaming with HD video support:

await client.init('en-US', 'Global', {
  patchJsMedia: true,
  webrtc: true  // Enable WebRTC mode
});

Feature Clients

Access specialized clients from the VideoClient:

ClientAccess MethodPurpose
Streamclient.getMediaStream()Video, audio, screen share, devices
Chatclient.getChatClient()Send/receive messages
Commandclient.getCommandClient()Custom commands (reactions, etc.)
Recordingclient.getRecordingClient()Cloud recording control
Transcriptionclient.getLiveTranscriptionClient()Live captions
LiveStreamclient.getLiveStreamClient()RTMP streaming
Subsessionclient.getSubsessionClient()Breakout rooms
Whiteboardclient.getWhiteboardClient()Collaborative whiteboard

Common Tasks

Start/Stop Video
await stream.startVideo();
await stream.stopVideo();
Start/Stop Audio
await stream.startAudio();
await stream.muteAudio();
await stream.unmuteAudio();
await stream.stopAudio();
Switch Devices
// Get available devices
const cameras = stream.getCameraList();
const mics = stream.getMicList();
const speakers = stream.getSpeakerList();

// Switch devices
await stream.switchCamera(cameraId);
await stream.switchMicrophone(micId);
await stream.switchSpeaker(speakerId);
Screen Sharing
// Start sharing
await stream.startShareScreen(canvas);

// Stop sharing
await stream.stopShareScreen();

// Receive share
client.on('active-share-change', async (payload) => {
  if (payload.state === 'Active') {
    await stream.startShareView(canvas, payload.userId);
  } else {
    await stream.stopShareView();
  }
});
Chat
const chatClient = client.getChatClient();

// Send to everyone
await chatClient.send('Hello, everyone!');

// Send to specific user
await chatClient.sendToUser(userId, 'Private message');

// Receive messages
client.on('chat-on-message', (payload) => {
  console.log(`${payload.sender.name}: ${payload.message}`);
});
Recording (Host Only)
const recordingClient = client.getRecordingClient();

await recordingClient.startCloudRecording();
await recordingClient.stopCloudRecording();

client.on('recording-change', (payload) => {
  console.log('Recording status:', payload.state);
});
Leave/End Session
// Leave session (others stay)
await client.leave();

// End session for ALL participants (host only)
await client.leave(true);

Error Handling

Common Join Errors
ErrorCauseSolution
Invalid signatureJWT expired or malformedGenerate new signature
Session does not existHost hasn't started yetShow "waiting" message, retry
Permission deniedUser denied camera/micRequest permission again
Example Error Handler
try {
  await client.join(topic, signature, userName, password);
} catch (error) {
  if (error.reason?.includes('signature')) {
    // Regenerate signature and retry
  } else if (error.reason?.includes('Session')) {
    // Show "Waiting for host..." and poll
  } else if (error.reason?.includes('Permission')) {
    // Guide user to enable permissions
  }
  console.error('Join failed:', error);
}

Browser Compatibility

FeatureChromeFirefoxSafariEdge
Video80+75+14+80+
Audio80+75+14+80+
Screen Share80+75+15+80+
Virtual BG80+90+-80+

Safari Notes:

  • Virtual background not supported

  • Screen sharing requires macOS 15+

CORS Errors (Telemetry)

CORS errors to log-external-gateway.zoom.us are harmless.

These are caused by COOP/COEP headers blocking telemetry requests. They don't affect SDK functionality.

Complete Documentation Library

Core Concepts
Complete Examples
Framework Integrations
Troubleshooting
References

Official Sample Repositories

Resources


Need help? Start with SKILL.md for complete navigation.

Merged from video-sdk/web/SKILL.md

Zoom Video SDK Web - Complete Documentation Index

Quick Start Path

If you're new to the SDK, follow this order:

  1. Read the architecture pattern → concepts/sdk-architecture-pattern.md

    • Universal formula: Create Client → Init → Join → Get Stream → Use

    • Once you understand this, you can implement any feature

  2. Implement session join → examples/session-join-pattern.md

    • Complete working JWT + session join code

  3. Listen to events → examples/event-handling.md

    • CRITICAL: The SDK is event-driven, you must listen for events

  4. Implement video → examples/video-rendering.md

    • Use attachVideo(), NOT renderVideo()

  5. Troubleshoot any issues → troubleshooting/common-issues.md

    • Quick diagnostic checklist

    • Error code tables


Documentation Structure

video-sdk/web/
├── SKILL.md                           # Main skill overview
├── SKILL.md                           # This file - navigation guide
│
├── concepts/                          # Core architectural patterns
│   ├── sdk-architecture-pattern.md   # Universal formula for ANY feature
│   └── singleton-hierarchy.md        # 4-level navigation guide
│
├── examples/                          # Complete working code
│   ├── session-join-pattern.md       # JWT auth + session join
│   ├── video-rendering.md            # attachVideo() patterns
│   ├── screen-share.md               # Send and receive screen shares
│   ├── event-handling.md             # Required events
│   ├── chat.md                       # Chat implementation
│   ├── command-channel.md            # Command channel messaging
│   ├── recording.md                  # Cloud recording control
│   ├── transcription.md              # Live transcription/captions
│   ├── react-hooks.md                # Official @zoom/videosdk-react library
│   └── framework-integrations.md     # Next.js, Vue/Nuxt, ZFG patterns
│
├── troubleshooting/                   # Problem solving guides
│   └── common-issues.md              # Quick diagnostic workflow
│
└── references/                        # Reference documentation
    ├── web-reference.md              # API hierarchy, methods, error codes
    └── events-reference.md           # All event types

By Use Case

I want to build a video app
  1. SDK Architecture Pattern - Understand the pattern

  2. Session Join Pattern - Join sessions

  3. Video Rendering - Display video

  4. Event Handling - Listen for video events

I'm getting runtime errors
  1. Common Issues - Error code tables

  2. "getMediaStream() is undefined" → Call AFTER join() completes

I want to receive screen shares
  1. Screen Share - startShareView() patterns

  2. Event Handling - active-share-change event

I want to send screen shares
  1. Screen Share - startShareScreen() patterns

  2. Check isStartShareScreenWithVideoElement() for element type

I want to use chat
  1. Chat - Send/receive messages

  2. getChatClient() for ChatClient access

I want to record sessions
  1. Recording - Cloud recording (host only)

  2. getRecordingClient() for RecordingClient access

I want to use live transcription
  1. Transcription - Enable live captions

  2. getLiveTranscriptionClient() for LiveTranscriptionClient access

I want to use command channel
  1. Command Channel - Custom signaling between participants

  2. Must call getCommandClient() AFTER join()

I want to implement a specific feature
  1. SDK Architecture Pattern - START HERE!

  2. Singleton Hierarchy - Navigate to the feature

  3. API Reference - Method signatures

I'm using React
  1. React Hooks - Official @zoom/videosdk-react library

  2. Provides hooks: useSession, useSessionUsers, useVideoState, useAudioState

  3. Pre-built components: VideoPlayerComponent, ScreenSharePlayerComponent

I'm using Next.js or Vue/Nuxt
  1. Framework Integrations - SSR considerations

  2. Server-side JWT generation patterns

  3. Client-side only SDK usage


Most Critical Documents

1. SDK Architecture Pattern (MASTER DOCUMENT)

concepts/sdk-architecture-pattern.md

The universal 5-step pattern:

  1. Create client

  2. Initialize SDK

  3. Join session

  4. Get stream

  5. Use features + listen to events

2. Common Issues (MOST COMMON PROBLEMS)

troubleshooting/common-issues.md

Common issues:

  • getMediaStream() returns undefined

  • Video not displaying

  • renderVideo() deprecated

3. Singleton Hierarchy (NAVIGATION MAP)

concepts/singleton-hierarchy.md

4-level deep navigation showing how to reach every feature.


Key Learnings

Critical Discoveries:
  1. getMediaStream() ONLY works after join()

  2. Use attachVideo() NOT renderVideo()

    • renderVideo() is deprecated

    • attachVideo() returns a VideoPlayer element to append to DOM

    • See: Video Rendering

  3. The SDK is Event-Driven

    • You MUST listen for events to render participant videos

    • key events: peer-video-state-change, user-added, user-removed

    • See: Event Handling

  4. Peer Videos on Mid-Session Join

    • Existing participants' videos won't auto-render

    • Must manually iterate getAllUser() and attachVideo()

    • See: Video Rendering

  5. CDN vs NPM

    • CDN exports as WebVideoSDK.default, not ZoomVideo

    • Some networks/ad blockers may block source.zoom.us - allowlist or use a permitted fallback strategy

    • See: Session Join Pattern

  6. SharedArrayBuffer for HD

    • Required for 720p/1080p video

    • Need COOP/COEP headers on server

    • Check with stream.isSupportHDVideo()

  7. Screen Share Element Type

    • Check isStartShareScreenWithVideoElement() for correct element type

    • See: Screen Share

  8. Command Channel Setup Order

    • Must call getCommandClient() AFTER client.join()

    • Register listeners AFTER join, not before

    • Web uses getCommandClient() not getCmdChannel()

    • See: Command Channel

  9. Command Channel is Session-Scoped

    • Does NOT span across different sessions

    • Both sender and receiver must be in the same session


Quick Reference

"getMediaStream() returns undefined"

→ Call AFTER join() completes

"Video not showing"

→ Video Rendering - Use attachVideo(), check events

"renderVideo() doesn't work"

→ Video Rendering - Use attachVideo() instead

"How do I implement [feature]?"

→ SDK Architecture Pattern

"How do I navigate to [client]?"

→ Singleton Hierarchy

"What error code means what?"

→ Common Issues


Document Version

Based on Zoom Video SDK for Web v2.3.x


Happy coding!

Remember: The SDK Architecture Pattern is your key to unlocking the entire SDK. Read it first!

Operations

  • RUNBOOK.md - 5-minute preflight and debugging checklist.

Files

examples/event-handling.md

Event Handling

Complete guide to handling events in the Zoom Video SDK for Web.

Event Registration Pattern

// Register event listener
client.on('event-name', (payload) => {
  // Handle event
});

// Remove event listener
client.off('event-name', handlerFunction);

Critical Events (Must Handle)

These events are essential for any Video SDK application.

Connection Events
// Connection state changes - REQUIRED
client.on('connection-change', (payload) => {
  const { state, reason } = payload;

  switch (state) {
    case 'Connected':
      console.log('Successfully connected to session');
      break;
    case 'Reconnecting':
      console.log('Connection lost, attempting to reconnect...', reason);
      showReconnectingUI();
      break;
    case 'Closed':
      console.log('Disconnected from session', reason);
      handleDisconnect(reason);
      break;
    case 'Fail':
      console.error('Connection failed', reason);
      break;
  }
});

function handleDisconnect(reason) {
  // Reasons: 'ended by host', 'kicked by host', 'session ended', etc.
  cleanup();
  showDisconnectMessage(reason);
}
Participant Events
// New participant joined
client.on('user-added', (payload) => {
  // payload is array of participants
  payload.forEach(user => {
    console.log('Joined:', user.displayName, 'ID:', user.userId);
    createParticipantUI(user);
  });
});

// Participant left
client.on('user-removed', (payload) => {
  payload.forEach(user => {
    console.log('Left:', user.displayName);
    removeParticipantUI(user.userId);
    // Clean up their video
    stream.detachVideo(user.userId).catch(() => {});
  });
});

// Participant updated (name, mute status, video status, etc.)
client.on('user-updated', (payload) => {
  payload.forEach(user => {
    console.log('Updated:', user);
    updateParticipantUI(user);
  });
});
Video Events
// Remote participant video state changed - CRITICAL
client.on('peer-video-state-change', async (payload) => {
  const { action, userId } = payload;

  if (action === 'Start') {
    // User started video - render it
    const element = await stream.attachVideo(userId, VideoQuality.Video_360P);
    document.getElementById(`video-${userId}`)?.appendChild(element);
  } else if (action === 'Stop') {
    // User stopped video - clean up
    await stream.detachVideo(userId);
    const container = document.getElementById(`video-${userId}`);
    if (container) container.innerHTML = '';
  }
});

// Local video capture state
client.on('video-capturing-change', (payload) => {
  const { state } = payload;
  // state: 'Started' | 'Stopped' | 'Failed'
  console.log('Video capture:', state);
});

// Video dimension changed
client.on('video-dimension-change', (payload) => {
  const { width, height, type } = payload;
  console.log('Video dimension:', width, 'x', height, type);
});
Audio Events
// Current user audio state changed
client.on('current-audio-change', (payload) => {
  const { action, type, source } = payload;

  switch (action) {
    case 'join':
      console.log('Joined audio via:', type); // 'computer' or 'phone'
      break;
    case 'leave':
      console.log('Left audio');
      break;
    case 'muted':
      console.log('Audio muted', source); // 'active', 'passive(mute all)', etc.
      updateMuteUI(true);
      break;
    case 'unmuted':
      console.log('Audio unmuted');
      updateMuteUI(false);
      break;
  }
});

// Host asks you to unmute
client.on('host-ask-unmute-audio', (payload) => {
  console.log('Host requested unmute:', payload.reason);
  showUnmutePrompt();
});

// Active speaker detection
client.on('active-speaker', (payload) => {
  // Array sorted by volume (loudest first)
  if (payload.length > 0) {
    const activeSpeaker = payload[0];
    highlightActiveSpeaker(activeSpeaker.userId);
  }
});

// Auto-play blocked by browser
client.on('auto-play-audio-failed', () => {
  // Show button for user to click to enable audio
  showAudioEnableButton();
});
Screen Share Events
// Someone started/stopped sharing
client.on('active-share-change', async (payload) => {
  const { state, userId } = payload;

  if (state === 'Active') {
    console.log('User', userId, 'started sharing');
    const element = await stream.attachShareView(userId);
    document.getElementById('share-container').appendChild(element);
    showShareView();
  } else {
    console.log('Sharing stopped');
    await stream.detachShareView(userId);
    hideShareView();
  }
});

// You were forced to stop sharing
client.on('passively-stop-share', (payload) => {
  console.log('Forced to stop sharing:', payload);
  // Reasons: 'PrivilegeChange', 'AnotherShareStarted', etc.
});

// Share privilege changed
client.on('share-privilege-change', (payload) => {
  console.log('Share privilege:', payload.privilege);
});

Chat Events

// Received a chat message
client.on('chat-on-message', (payload) => {
  const { sender, receiver, message, timestamp, id } = payload;

  console.log(`[${sender.name}]: ${message}`);
  addChatMessage(payload);
});

// Chat privilege changed by host
client.on('chat-privilege-change', (payload) => {
  const { chatPrivilege } = payload;
  // ChatPrivilege.All, ChatPrivilege.NoOne, ChatPrivilege.EveryonePublicly
  updateChatUI(chatPrivilege);
});

Recording Events

// Cloud recording state changed
client.on('recording-change', (payload) => {
  const { state } = payload;
  // state: 'Recording', 'Paused', 'Stopped', etc.

  if (state === 'Recording') {
    showRecordingIndicator();
  } else {
    hideRecordingIndicator();
  }
});

// Individual recording consent
client.on('individual-recording-change', (payload) => {
  const { state, userId } = payload;

  if (state === 'Ask') {
    // Host is asking for recording consent
    showRecordingConsentDialog();
  }
});

Live Transcription Events

// Transcription status changed
client.on('caption-status', (payload) => {
  const { autoCaption, language } = payload;
  console.log('Caption enabled:', autoCaption, 'Language:', language);
});

// Received transcription text
client.on('caption-message', (payload) => {
  const { text, displayName, done, userId } = payload;

  // done=false means partial/interim result
  // done=true means final result
  updateTranscription(payload);
});

// Captions enabled/disabled
client.on('caption-enable', (isEnabled) => {
  console.log('Captions:', isEnabled ? 'enabled' : 'disabled');
});

Device Events

// Device plugged in or removed
client.on('device-change', () => {
  console.log('Device list changed');
  // Refresh device lists
  updateCameraList(stream.getCameraList());
  updateMicList(stream.getMicList());
  updateSpeakerList(stream.getSpeakerList());
});

// Device permission changed
client.on('device-permission-change', (payload) => {
  const { name, state } = payload;
  // name: 'microphone' | 'camera'
  // state: 'granted' | 'denied' | 'prompt'
  console.log(`${name} permission: ${state}`);
});

Network Quality Events

// Network quality changed
client.on('network-quality-change', (payload) => {
  const { userId, type, level } = payload;
  // type: 'uplink' | 'downlink'
  // level: 0-5 (0=unknown, 1=bad, 5=excellent)

  if (level <= 2) {
    showNetworkWarning(userId, type);
  }
});

Statistics Events

// Video statistics
client.on('video-statistic-data-change', (payload) => {
  const { data, type } = payload;
  // data.encoding: true=send, false=receive
  console.log('Video stats:', {
    fps: data.fps,
    resolution: `${data.width}x${data.height}`,
    bitrate: data.bitrate,
    packetLoss: data.avg_loss,
  });
});

// Audio statistics
client.on('audio-statistic-data-change', (payload) => {
  const { data, type } = payload;
  console.log('Audio stats:', {
    bitrate: data.bitrate,
    packetLoss: data.avg_loss,
    jitter: data.jitter,
  });
});

Complete Event Handler Setup

class ZoomEventHandler {
  constructor(client, stream) {
    this.client = client;
    this.stream = stream;
    this.handlers = new Map();
  }

  init() {
    this.setupConnectionEvents();
    this.setupParticipantEvents();
    this.setupVideoEvents();
    this.setupAudioEvents();
    this.setupShareEvents();
    this.setupChatEvents();
    this.setupRecordingEvents();
    this.setupDeviceEvents();
  }

  setupConnectionEvents() {
    this.on('connection-change', this.handleConnectionChange.bind(this));
  }

  setupParticipantEvents() {
    this.on('user-added', this.handleUserAdded.bind(this));
    this.on('user-removed', this.handleUserRemoved.bind(this));
    this.on('user-updated', this.handleUserUpdated.bind(this));
  }

  setupVideoEvents() {
    this.on('peer-video-state-change', this.handlePeerVideoChange.bind(this));
    this.on('video-capturing-change', this.handleCapturingChange.bind(this));
  }

  setupAudioEvents() {
    this.on('current-audio-change', this.handleAudioChange.bind(this));
    this.on('host-ask-unmute-audio', this.handleUnmuteRequest.bind(this));
    this.on('active-speaker', this.handleActiveSpeaker.bind(this));
    this.on('auto-play-audio-failed', this.handleAutoPlayFailed.bind(this));
  }

  setupShareEvents() {
    this.on('active-share-change', this.handleShareChange.bind(this));
    this.on('passively-stop-share', this.handlePassiveStopShare.bind(this));
  }

  setupChatEvents() {
    this.on('chat-on-message', this.handleChatMessage.bind(this));
    this.on('chat-privilege-change', this.handleChatPrivilegeChange.bind(this));
  }

  setupRecordingEvents() {
    this.on('recording-change', this.handleRecordingChange.bind(this));
  }

  setupDeviceEvents() {
    this.on('device-change', this.handleDeviceChange.bind(this));
  }

  // Helper to track handlers for cleanup
  on(event, handler) {
    this.client.on(event, handler);
    this.handlers.set(event, handler);
  }

  // Clean up all handlers
  destroy() {
    this.handlers.forEach((handler, event) => {
      this.client.off(event, handler);
    });
    this.handlers.clear();
  }

  // Event handlers
  handleConnectionChange(payload) {
    console.log('Connection:', payload.state, payload.reason);
  }

  handleUserAdded(payload) {
    payload.forEach(user => console.log('User joined:', user.displayName));
  }

  handleUserRemoved(payload) {
    payload.forEach(user => {
      console.log('User left:', user.displayName);
      this.stream.detachVideo(user.userId).catch(() => {});
    });
  }

  handleUserUpdated(payload) {
    payload.forEach(user => console.log('User updated:', user));
  }

  async handlePeerVideoChange(payload) {
    const { action, userId } = payload;
    if (action === 'Start') {
      const element = await this.stream.attachVideo(userId, VideoQuality.Video_360P);
      document.getElementById(`video-${userId}`)?.appendChild(element);
    } else {
      await this.stream.detachVideo(userId);
    }
  }

  handleCapturingChange(payload) {
    console.log('Video capture:', payload.state);
  }

  handleAudioChange(payload) {
    console.log('Audio:', payload.action, payload.type);
  }

  handleUnmuteRequest(payload) {
    console.log('Host requested unmute');
  }

  handleActiveSpeaker(payload) {
    if (payload.length > 0) {
      console.log('Active speaker:', payload[0].userId);
    }
  }

  handleAutoPlayFailed() {
    console.log('Auto-play blocked - show enable audio button');
  }

  async handleShareChange(payload) {
    const { state, userId } = payload;
    if (state === 'Active') {
      const element = await this.stream.attachShareView(userId);
      document.getElementById('share-container')?.appendChild(element);
    } else {
      await this.stream.detachShareView(userId);
    }
  }

  handlePassiveStopShare(payload) {
    console.log('Forced to stop sharing:', payload);
  }

  handleChatMessage(payload) {
    console.log(`Chat from ${payload.sender.name}: ${payload.message}`);
  }

  handleChatPrivilegeChange(payload) {
    console.log('Chat privilege:', payload.chatPrivilege);
  }

  handleRecordingChange(payload) {
    console.log('Recording:', payload.state);
  }

  handleDeviceChange() {
    console.log('Device list changed');
  }
}

// Usage
const eventHandler = new ZoomEventHandler(client, stream);
eventHandler.init();

// Cleanup on leave
client.on('connection-change', (payload) => {
  if (payload.state === 'Closed') {
    eventHandler.destroy();
  }
});

Event Reference Table

EventCategoryWhen Triggered
connection-changeSessionConnection state changes
user-addedParticipantNew participant joins
user-removedParticipantParticipant leaves
user-updatedParticipantParticipant properties change
peer-video-state-changeVideoRemote video starts/stops
video-capturing-changeVideoLocal capture state changes
video-dimension-changeVideoVideo resolution changes
current-audio-changeAudioLocal audio state changes
host-ask-unmute-audioAudioHost requests unmute
active-speakerAudioActive speaker changes
auto-play-audio-failedAudioBrowser blocked autoplay
active-share-changeShareSomeone starts/stops sharing
passively-stop-shareShareForced to stop sharing
share-privilege-changeShareShare settings changed
chat-on-messageChatMessage received
recording-changeRecordingRecording state changes
caption-messageTranscriptionTranscription text received
device-changeDeviceDevice added/removed
network-quality-changeNetworkNetwork quality changes

Key Points

  1. Always handle connection-change - Essential for detecting disconnects

  2. peer-video-state-change is for remote video - Not your own video

  3. Clean up on user-removed - Detach their video

  4. Handle auto-play-audio-failed - Browser may block audio autoplay

  5. Use off() to clean up - Prevent memory leaks

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.

Needs: Needs network access, Needs Node.js

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/video-sdk/web · 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-video-sdk-web.zip