plugins/zoom/skills/video-sdk/web/SKILL.md
Zoom Video SDK for Web - JavaScript/TypeScript integration for browser-based video sessions, real-time communication, screen sharing, recording, and live transcription
npx skillsauth add openai/plugins zoom-video-sdk-webInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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:
Reference:
Having issues?
The Zoom Video SDK for Web is a JavaScript library that provides:
// 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:
allow, warn, block).Cross-skill flow: ../../general/use-cases/probe-sdk-preflight-readiness-gate.md
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;
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);
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!
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
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!
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);
});
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);
}
}
};
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;
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).
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 });
}
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);
}
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
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);
}
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;
}
}
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
});
Access specialized clients from the VideoClient:
| Client | Access Method | Purpose |
|--------|---------------|---------|
| Stream | client.getMediaStream() | Video, audio, screen share, devices |
| Chat | client.getChatClient() | Send/receive messages |
| Command | client.getCommandClient() | Custom commands (reactions, etc.) |
| Recording | client.getRecordingClient() | Cloud recording control |
| Transcription | client.getLiveTranscriptionClient() | Live captions |
| LiveStream | client.getLiveStreamClient() | RTMP streaming |
| Subsession | client.getSubsessionClient() | Breakout rooms |
| Whiteboard | client.getWhiteboardClient() | Collaborative whiteboard |
await stream.startVideo();
await stream.stopVideo();
await stream.startAudio();
await stream.muteAudio();
await stream.unmuteAudio();
await stream.stopAudio();
// 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);
// 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();
}
});
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}`);
});
const recordingClient = client.getRecordingClient();
await recordingClient.startCloudRecording();
await recordingClient.stopCloudRecording();
client.on('recording-change', (payload) => {
console.log('Recording status:', payload.state);
});
// Leave session (others stay)
await client.leave();
// End session for ALL participants (host only)
await client.leave(true);
| Error | Cause | Solution |
|-------|-------|----------|
| Invalid signature | JWT expired or malformed | Generate new signature |
| Session does not exist | Host hasn't started yet | Show "waiting" message, retry |
| Permission denied | User denied camera/mic | Request permission again |
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);
}
| Feature | Chrome | Firefox | Safari | Edge | |---------|--------|---------|--------|------| | Video | 80+ | 75+ | 14+ | 80+ | | Audio | 80+ | 75+ | 14+ | 80+ | | Screen Share | 80+ | 75+ | 15+ | 80+ | | Virtual BG | 80+ | 90+ | - | 80+ |
Safari Notes:
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.
| Type | Repository | |------|------------| | Web Sample | videosdk-web-sample | | React SDK | videosdk-react | | Next.js | videosdk-nextjs-quickstart | | Vue/Nuxt | videosdk-vue-nuxt-quickstart | | Auth Endpoint | videosdk-auth-endpoint-sample | | UI Toolkit | videosdk-zoom-ui-toolkit-react-sample |
Need help? Start with SKILL.md for complete navigation.
If you're new to the SDK, follow this order:
Read the architecture pattern → concepts/sdk-architecture-pattern.md
Implement session join → examples/session-join-pattern.md
Listen to events → examples/event-handling.md
Implement video → examples/video-rendering.md
Troubleshoot any issues → troubleshooting/common-issues.md
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
concepts/sdk-architecture-pattern.md
The universal 5-step pattern:
troubleshooting/common-issues.md
Common issues:
concepts/singleton-hierarchy.md
4-level deep navigation showing how to reach every feature.
getMediaStream() ONLY works after join()
Use attachVideo() NOT renderVideo()
The SDK is Event-Driven
Peer Videos on Mid-Session Join
CDN vs NPM
WebVideoSDK.default, not ZoomVideosource.zoom.us - allowlist or use a permitted fallback strategySharedArrayBuffer for HD
stream.isSupportHDVideo()Screen Share Element Type
isStartShareScreenWithVideoElement() for correct element typeCommand Channel Setup Order
Command Channel is Session-Scoped
→ Call AFTER join() completes
→ Video Rendering - Use attachVideo(), check events
→ Video Rendering - Use attachVideo() instead
→ SDK Architecture Pattern
→ Singleton Hierarchy
→ Common Issues
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!
tools
Top-level workflow skill for USD performance diagnosis and optimization. Use for slow loading, high memory, low FPS, or 'optimize my scene' requests; delegates auth/runtime setup to Phase 0 owners.
data-ai
Use when the user mentions MagicPath, designs, UI components, themes, canvas selections, or repo-to-canvas UI work; run magicpath-ai to search, inspect, install, or author components.
documentation
Use as the top-level router for Omniverse Realtime Viewer USD app requests and focused viewer reference documents.
tools
Turn Notion specs into implementation plans, tasks, and progress tracking; use when implementing PRDs/feature specs and creating Notion plans + tasks from them.