Video SDK Web
AnthropicOfficialCreated by ZoomDistributed by Anthropic
- Skill
- Tool integration
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The Zoom Video SDK for Web provides a library for developers to build custom video-based experiences directly within browser environments. It supports real-time communication, screen sharing, cloud recording, live streaming, chat, and live transcription. The SDK requires a specific lifecycle, including client initialization and session joining, and provides specialized clients for managing media streams, chat, and other collaborative features.
What it does
- Enables custom video and audio sessions in modern web browsers.
- Supports screen sharing and collaborative whiteboard features.
- Provides tools for cloud recording and live streaming to RTMP endpoints.
- Includes messaging capabilities via chat and command channels.
- Facilitates live transcription for real-time speech-to-text.
- Allows for virtual backgrounds and custom video frame processing.
When to use it
- When building a custom video application that requires a unique UI.
- When integrating real-time video communication into an existing web platform.
- When implementing features like breakout rooms or live transcription in a web-based session.
- When you need to control media stream rendering and participant interaction programmatically.
Advantages
- Provides a modular API for granular control over media and session features.
- Supports standard web technologies like JavaScript and TypeScript.
- Offers specialized clients for distinct features like recording and chat.
- Includes event-driven architecture for handling participant and media state changes.
Usage examples
- “How do I join a video session using the Zoom Video SDK for Web?”
- Initialize the client with your credentials, call the join method with the session topic and signature, then retrieve the media stream object to start video and audio.
- “How can I display a remote participant's video?”
- Listen for the peer-video-state-change event, and when a participant starts their video, use the attachVideo method to append their video element to your DOM.
- “What is the correct way to handle media streams?”
- Always call getMediaStream after the join method has successfully completed to ensure the stream object is available.
- “How do I implement screen sharing?”
- Use the startShareScreen method on the media stream object, passing in a canvas or video element to render the shared content.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/anthropics/video-sdk-web/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip 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
Quick Links
New to Video SDK? Follow this path:
SDK Architecture Pattern - Universal 3-step pattern for ANY feature
Session Join Pattern - Complete working code to join a session
Video Rendering - Display video with attachVideo()
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);Optional Pre-Join Diagnostics (Recommended for Reliability)
Use Probe SDK as a readiness gate before client.join(...) when you need to reduce failed starts:
Run diagnostics with ../../probe-sdk/SKILL.md.
Evaluate policy (
allow,warn,block).Start Video SDK join only when policy allows.
Cross-skill flow: ../../general/use-cases/probe-sdk-preflight-readiness-gate.md
Installation
NPM (Recommended)
npm install @zoom/videosdkimport ZoomVideo from '@zoom/videosdk';CDN (Fallback Strategy Recommended)
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(); // WorksUse 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-corpNote: 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 // 4Virtual 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:
| 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 |
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
| 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 |
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
| 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:
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
SDK Architecture Pattern - Universal 3-step pattern for ANY feature
Singleton Hierarchy - 4-level navigation guide
Complete Examples
Session Join Pattern - JWT auth + session join with full code
Video Rendering - attachVideo() patterns
Screen Share - Send and receive screen shares
Event Handling - Required events
Chat - In-session messaging
Recording - Cloud recording control
Transcription - Live captions
Framework Integrations
React Hooks - Official @zoom/videosdk-react library
Framework Integrations - Next.js, Vue/Nuxt patterns
Troubleshooting
Common Issues - Quick diagnostics & error codes
References
API Reference - Complete method signatures
Events Reference - All event types
SKILL.md - Complete navigation guide
Official Sample Repositories
| 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 |
Resources
Official Docs: https://developers.zoom.us/docs/video-sdk/web/
API Reference: https://marketplacefront.zoom.us/sdk/custom/web/modules.html
Dev Forum: https://devforum.zoom.us/
GitHub Samples: https://github.com/zoom/videosdk-web-sample
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:
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
Implement session join → examples/session-join-pattern.md
Complete working JWT + session join code
Listen to events → examples/event-handling.md
CRITICAL: The SDK is event-driven, you must listen for events
Implement video → examples/video-rendering.md
Use attachVideo(), NOT renderVideo()
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 typesBy Use Case
I want to build a video app
SDK Architecture Pattern - Understand the pattern
Session Join Pattern - Join sessions
Video Rendering - Display video
Event Handling - Listen for video events
I'm getting runtime errors
Common Issues - Error code tables
"getMediaStream() is undefined" → Call AFTER join() completes
I want to receive screen shares
Screen Share - startShareView() patterns
Event Handling - active-share-change event
I want to send screen shares
Screen Share - startShareScreen() patterns
Check isStartShareScreenWithVideoElement() for element type
I want to use chat
Chat - Send/receive messages
getChatClient() for ChatClient access
I want to record sessions
Recording - Cloud recording (host only)
getRecordingClient() for RecordingClient access
I want to use live transcription
Transcription - Enable live captions
getLiveTranscriptionClient() for LiveTranscriptionClient access
I want to use command channel
Command Channel - Custom signaling between participants
Must call getCommandClient() AFTER join()
I want to implement a specific feature
SDK Architecture Pattern - START HERE!
Singleton Hierarchy - Navigate to the feature
API Reference - Method signatures
I'm using React
React Hooks - Official @zoom/videosdk-react library
Provides hooks: useSession, useSessionUsers, useVideoState, useAudioState
Pre-built components: VideoPlayerComponent, ScreenSharePlayerComponent
I'm using Next.js or Vue/Nuxt
Framework Integrations - SSR considerations
Server-side JWT generation patterns
Client-side only SDK usage
Most Critical Documents
1. SDK Architecture Pattern (MASTER DOCUMENT)
concepts/sdk-architecture-pattern.md
The universal 5-step pattern:
Create client
Initialize SDK
Join session
Get stream
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:
getMediaStream() ONLY works after join()
The stream object is not available until session is joined
Use attachVideo() NOT renderVideo()
renderVideo() is deprecated
attachVideo() returns a VideoPlayer element to append to DOM
See: Video Rendering
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
Peer Videos on Mid-Session Join
Existing participants' videos won't auto-render
Must manually iterate getAllUser() and attachVideo()
See: Video Rendering
CDN vs NPM
CDN exports as
WebVideoSDK.default, notZoomVideoSome networks/ad blockers may block
source.zoom.us- allowlist or use a permitted fallback strategySee: Session Join Pattern
SharedArrayBuffer for HD
Required for 720p/1080p video
Need COOP/COEP headers on server
Check with
stream.isSupportHDVideo()
Screen Share Element Type
Check
isStartShareScreenWithVideoElement()for correct element typeSee: Screen Share
Command Channel Setup Order
Must call getCommandClient() AFTER client.join()
Register listeners AFTER join, not before
Web uses getCommandClient() not getCmdChannel()
See: Command Channel
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]?"
"How do I navigate to [client]?"
"What error code means what?"
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
- ATTRIBUTION.mdAdded by AutoGineer982 B
- LICENSELicence file added by AutoGineer1.1 KiB
- RUNBOOK.md2.5 KiB
- SKILL.md25.8 KiB
- concepts/sdk-architecture-pattern.md8.3 KiB
- concepts/singleton-hierarchy.md18.9 KiB
- examples/chat.md12.0 KiB
- examples/command-channel.md7.1 KiB
- examples/event-handling.md14.1 KiB
- examples/framework-integrations.md10.8 KiB
- examples/react-hooks.md9.8 KiB
- examples/recording.md10.4 KiB
- examples/screen-share.md12.1 KiB
- examples/session-join-pattern.md10.2 KiB
- examples/transcription.md13.9 KiB
- examples/video-rendering.md10.0 KiB
- references/events-reference.md12.8 KiB
- references/web-reference.md15.5 KiB
- references/web.md26.2 KiB
- troubleshooting/common-issues.md11.0 KiB
examples/transcription.md
Live Transcription
Complete guide to implementing live transcription in the Zoom Video SDK for Web.
Prerequisites
Live transcription must be enabled on your Zoom account
Session must have live transcription feature available
Getting the Transcription Client
// Get transcription client after joining session
const transcriptionClient = client.getLiveTranscriptionClient();Basic Operations
Start Live Transcription
try {
await transcriptionClient.startLiveTranscription();
console.log('Live transcription started');
} catch (error) {
console.error('Failed to start transcription:', error);
}Check Transcription Status
const status = transcriptionClient.getLiveTranscriptionStatus();
console.log('Transcription enabled:', status.isLiveTranscriptionEnabled);
console.log('Caption language:', status.captionLanguage);
console.log('Translation settings:', status.translatedSetting);Set Speaking Language
import { LiveTranscriptionLanguage } from '@zoom/videosdk';
// Set the language you're speaking
await transcriptionClient.setSpeakingLanguage(
LiveTranscriptionLanguage.English
);Set Translation Language
// Translate transcriptions to another language
await transcriptionClient.setTranslationLanguage(
LiveTranscriptionLanguage.Spanish
);
// Disable translation
await transcriptionClient.setTranslationLanguage(); // No argumentReceiving Transcription Messages
// Listen for transcription text
client.on('caption-message', (payload) => {
const {
msgId, // Message ID
text, // Transcribed text
userId, // Speaker's user ID
displayName, // Speaker's display name
source, // 'caption' or 'translation'
language, // Language code
done, // true = final, false = interim
timestamp, // Unix timestamp
} = payload;
if (done) {
// Final transcription - display permanently
addFinalTranscription(displayName, text);
} else {
// Interim result - update in place
updateInterimTranscription(displayName, text);
}
});Transcription Events
Caption Status Changes
client.on('caption-status', (payload) => {
const {
autoCaption, // Auto-captioning enabled
language, // Caption language name
lang, // Language code
sessionLanguage, // Session's transcription language
translationStarted, // Translation active
} = payload;
console.log('Caption status:', payload);
});Captions Enabled/Disabled
client.on('caption-enable', (isEnabled) => {
if (isEnabled) {
showCaptionUI();
} else {
hideCaptionUI();
}
});Caption Language Locked
client.on('caption-language-lock', (isLocked) => {
if (isLocked) {
// Host has locked the transcription language
disableLanguageSelector();
} else {
enableLanguageSelector();
}
});Host Disabled Captions
client.on('caption-host-disable', (isDisabled) => {
if (isDisabled) {
console.log('Host has disabled captions');
hideCaptionUI();
}
});Get Transcription History
// Get full transcription history
const history = transcriptionClient.getFullTranscriptionHistory();
// May return Promise for large histories (100,000+ records)
if (history instanceof Promise) {
const records = await history;
displayHistory(records);
} else {
displayHistory(history);
}
// Get latest transcription
const latest = transcriptionClient.getLatestTranscription();
console.log('Latest:', latest);
// Get latest translation
const latestTranslation = transcriptionClient.getLatestTranslation();
console.log('Latest translation:', latestTranslation);Get Current Languages
// Get current transcription language
const transcriptionLang = transcriptionClient.getCurrentTranscriptionLanguage();
console.log('Transcription language:', transcriptionLang);
// Get current translation language
const translationLang = transcriptionClient.getCurrentTranslationLanguage();
console.log('Translation language:', translationLang);Host Controls
Lock Transcription Language
// Host can lock the transcription language
await transcriptionClient.lockTranscriptionLanguage(true); // Lock
await transcriptionClient.lockTranscriptionLanguage(false); // UnlockDisable Captions
// Host can disable captions for everyone
await transcriptionClient.disableCaptions(true); // Disable
await transcriptionClient.disableCaptions(false); // EnableComplete Transcription Manager
import { LiveTranscriptionLanguage } from '@zoom/videosdk';
class TranscriptionManager {
constructor(client) {
this.client = client;
this.transcriptionClient = null;
this.transcriptions = [];
this.interimMap = new Map(); // Track interim results by speaker
this.onTranscriptionUpdate = null;
this.onStatusChange = null;
}
init() {
this.transcriptionClient = this.client.getLiveTranscriptionClient();
this.setupEventListeners();
}
setupEventListeners() {
// Transcription messages
this.client.on('caption-message', (payload) => {
this.handleCaptionMessage(payload);
});
// Status changes
this.client.on('caption-status', (payload) => {
if (this.onStatusChange) {
this.onStatusChange(payload);
}
});
// Captions enabled/disabled
this.client.on('caption-enable', (isEnabled) => {
if (this.onStatusChange) {
this.onStatusChange({ enabled: isEnabled });
}
});
}
handleCaptionMessage(payload) {
const { userId, displayName, text, done, source, timestamp } = payload;
const key = `${userId}-${source}`;
if (done) {
// Final result - move from interim to final
this.interimMap.delete(key);
this.transcriptions.push({
userId,
displayName,
text,
source,
timestamp,
isFinal: true,
});
} else {
// Interim result - update in place
this.interimMap.set(key, {
userId,
displayName,
text,
source,
timestamp,
isFinal: false,
});
}
if (this.onTranscriptionUpdate) {
this.onTranscriptionUpdate(this.getAllTranscriptions());
}
}
getAllTranscriptions() {
// Combine final transcriptions with current interim results
const interim = Array.from(this.interimMap.values());
return [...this.transcriptions, ...interim];
}
async start() {
await this.transcriptionClient.startLiveTranscription();
}
async setSpeakingLanguage(language) {
await this.transcriptionClient.setSpeakingLanguage(language);
}
async setTranslationLanguage(language) {
await this.transcriptionClient.setTranslationLanguage(language);
}
async disableTranslation() {
await this.transcriptionClient.setTranslationLanguage();
}
getStatus() {
return this.transcriptionClient.getLiveTranscriptionStatus();
}
async getHistory() {
const history = this.transcriptionClient.getFullTranscriptionHistory();
if (history instanceof Promise) {
return await history;
}
return history;
}
}
// Usage
const transcriptionManager = new TranscriptionManager(client);
transcriptionManager.init();
transcriptionManager.onTranscriptionUpdate = (transcriptions) => {
renderTranscriptions(transcriptions);
};
// Start transcription
document.getElementById('start-captions').onclick = async () => {
await transcriptionManager.start();
};
// Change language
document.getElementById('language-select').onchange = async (e) => {
await transcriptionManager.setSpeakingLanguage(e.target.value);
};React Component
import React, { useState, useEffect, useRef } from 'react';
import { VideoClient, LiveTranscriptionLanguage } from '@zoom/videosdk';
interface TranscriptionEntry {
displayName: string;
text: string;
timestamp: number;
isFinal: boolean;
source: string;
}
interface TranscriptionProps {
client: typeof VideoClient;
isHost: boolean;
}
export const LiveTranscription: React.FC<TranscriptionProps> = ({
client,
isHost
}) => {
const [transcriptions, setTranscriptions] = useState<TranscriptionEntry[]>([]);
const [interimMap] = useState(new Map<string, TranscriptionEntry>());
const [isEnabled, setIsEnabled] = useState(false);
const [speakingLanguage, setSpeakingLanguage] = useState<string>('');
const containerRef = useRef<HTMLDivElement>(null);
const transcriptionClient = client.getLiveTranscriptionClient();
useEffect(() => {
// Get initial status
const status = transcriptionClient.getLiveTranscriptionStatus();
setIsEnabled(status.isLiveTranscriptionEnabled);
// Handle caption messages
const handleCaption = (payload: any) => {
const { userId, displayName, text, done, source, timestamp } = payload;
const key = `${userId}-${source}`;
if (done) {
interimMap.delete(key);
setTranscriptions(prev => [...prev, {
displayName,
text,
timestamp,
isFinal: true,
source,
}]);
} else {
interimMap.set(key, {
displayName,
text,
timestamp,
isFinal: false,
source,
});
// Force re-render
setTranscriptions(prev => [...prev]);
}
};
const handleEnable = (enabled: boolean) => {
setIsEnabled(enabled);
};
client.on('caption-message', handleCaption);
client.on('caption-enable', handleEnable);
return () => {
client.off('caption-message', handleCaption);
client.off('caption-enable', handleEnable);
};
}, [client, transcriptionClient, interimMap]);
// Auto-scroll
useEffect(() => {
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
}
}, [transcriptions]);
const startTranscription = async () => {
await transcriptionClient.startLiveTranscription();
};
const handleLanguageChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
const language = e.target.value as LiveTranscriptionLanguage;
setSpeakingLanguage(language);
await transcriptionClient.setSpeakingLanguage(language);
};
// Combine final and interim transcriptions for display
const allTranscriptions = [
...transcriptions,
...Array.from(interimMap.values()),
].sort((a, b) => a.timestamp - b.timestamp);
return (
<div className="transcription-panel">
<div className="transcription-header">
<h3>Live Transcription</h3>
{!isEnabled && (
<button onClick={startTranscription}>
Enable Captions
</button>
)}
<select value={speakingLanguage} onChange={handleLanguageChange}>
<option value="">Select Language</option>
<option value={LiveTranscriptionLanguage.English}>English</option>
<option value={LiveTranscriptionLanguage.Spanish}>Spanish</option>
<option value={LiveTranscriptionLanguage.French}>French</option>
<option value={LiveTranscriptionLanguage.German}>German</option>
<option value={LiveTranscriptionLanguage.Chinese}>Chinese</option>
<option value={LiveTranscriptionLanguage.Japanese}>Japanese</option>
</select>
</div>
<div className="transcription-content" ref={containerRef}>
{allTranscriptions.map((entry, index) => (
<div
key={`${entry.timestamp}-${index}`}
className={`transcription-entry ${entry.isFinal ? 'final' : 'interim'}`}
>
<span className="speaker">{entry.displayName}:</span>
<span className="text">{entry.text}</span>
</div>
))}
</div>
<style jsx>{`
.transcription-panel {
display: flex;
flex-direction: column;
height: 300px;
border: 1px solid #ddd;
border-radius: 8px;
}
.transcription-header {
padding: 10px;
border-bottom: 1px solid #ddd;
display: flex;
align-items: center;
gap: 10px;
}
.transcription-content {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.transcription-entry {
margin-bottom: 8px;
}
.transcription-entry.interim {
color: #888;
font-style: italic;
}
.speaker {
font-weight: bold;
margin-right: 5px;
}
`}</style>
</div>
);
};Available Languages
// LiveTranscriptionLanguage enum
LiveTranscriptionLanguage.English
LiveTranscriptionLanguage.Spanish
LiveTranscriptionLanguage.French
LiveTranscriptionLanguage.German
LiveTranscriptionLanguage.Italian
LiveTranscriptionLanguage.Portuguese
LiveTranscriptionLanguage.Russian
LiveTranscriptionLanguage.Chinese
LiveTranscriptionLanguage.Japanese
LiveTranscriptionLanguage.Korean
// ... and moreKey Points
Interim vs Final -
done=falseis interim (updating),done=trueis finalStart transcription - Call
startLiveTranscription()to enableSet speaking language - Tell the system what language you're speaking
Translation is optional - Use
setTranslationLanguage()if neededHandle large histories -
getFullTranscriptionHistory()may return Promise
Key Events
| Event | When | Payload |
|---|---|---|
caption-message | Transcription text received | Text, speaker, done flag |
caption-status | Status changes | Language, enabled state |
caption-enable | Captions enabled/disabled | Boolean |
caption-language-lock | Language locked by host | Boolean |
caption-host-disable | Host disabled captions | Boolean |
Related Documentation
Event Handling - Transcription events
API Reference - Full LiveTranscriptionClient API
Requirements and compatibility
Written for: Claude Code, Claude apps
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: Claude Code frontmatter, 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
- Anthropic (Official)
- Distributed by
- Anthropic
- Source
- anthropics/knowledge-work-plugins · partner-built/zoom-plugin/skills/video-sdk/web · commit e050c2e
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from partner-built/zoom-plugin/LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Zoom or Anthropic.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download video-sdk-web.zip