Zoom Video SDK for Web - JavaScript/TypeScript integration for browser-based video sessions, real-time communication, screen sharing, recording, and live transcription
65
80%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Fix and improve this skill with Tessl
tessl review fix ./plugins/zoom/skills/video-sdk/web/SKILL.mdExpert 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/videosdkimport 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(); // WorksrenderVideo() 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-corpNote: 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 // 4const 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 typesconcepts/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
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!
11c74d6
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.