Zoom Video SDK Web
This skill helps developers integrate Zoom Video SDK for Web into an existing
frontend application. Optimize for working code first, then add feature-specific
behavior and troubleshooting.
The primary build guidance and nested references are synchronized from the
official zoom/videosdk-web repository at SDK version 2.5.0. Repo-local
concepts and examples complement that upstream material rather than override it.
Use repo-local official docs under references/ as the primary source of truth.
Use troubleshooting/ for symptom-driven debugging.
Scope
Use this skill for:
- Browser integrations using
@zoom/videosdk.
- Custom session UI: audio, video, screen share, chat, command channel,
recording, subsessions, transcription/translation, PSTN/SIP, whiteboard,
preview devices, and quality reporting.
- Debugging rejected Promises, SDK error codes, browser support, rendering
issues, and event/lifecycle bugs.
Do not use this skill for:
- Joining or embedding regular Zoom Meetings or Webinars with Meeting SDK.
- Generic WebRTC advice that bypasses Video SDK APIs.
- Server-side Video SDK APIs except when generating the client JWT.
Operating Principles
Never invent SDK API surface. Do not guess, recall, or "reconstruct" method
names, event strings, enum values, option fields, or payload shapes from memory.
The SDK surface changes across versions and plausible-looking APIs are easy to
hallucinate. Before writing any SDK call, event name, or enum, confirm it exists
in the bundled type definitions (node_modules/@zoom/videosdk/dist/types/). If a
symbol cannot be verified there, say so explicitly and check the types or ask —
do not emit a plausible guess. This rule overrides convenience and speed.
- Start with the shortest runnable path for the developer's current project.
- Detect the current framework from the repo before giving framework-specific
code.
- Read the relevant docs before making non-trivial claims or changes.
- Prefer official
references/ content over memory.
- Do not provide large API inventories in the answer unless the user asks.
- Treat most SDK operations as async Promise-returning operations; prevent
duplicate in-flight calls in UI.
- Use
error.errorCode for actionable troubleshooting.
Useful type entry points (paths relative to your project root, after
npm install @zoom/videosdk):
- Session/client:
node_modules/@zoom/videosdk/dist/types/videoclient.d.ts
- Events:
node_modules/@zoom/videosdk/dist/types/event-callback.d.ts
- Audio / video / screen share:
node_modules/@zoom/videosdk/dist/types/media.d.ts
- Chat:
node_modules/@zoom/videosdk/dist/types/chat.d.ts
- Command channel:
node_modules/@zoom/videosdk/dist/types/command.d.ts
- Recording:
node_modules/@zoom/videosdk/dist/types/recording.d.ts
- Transcription:
node_modules/@zoom/videosdk/dist/types/live-transcription.d.ts
- Error codes:
node_modules/@zoom/videosdk/dist/types/exception-code.d.ts
When the package is not installed, or when an AI-readable declaration inventory
is more efficient, read references/type-definitions-json.md and then only the
matching file under references/types-json/. These generated files describe
@zoom/videosdk 2.5.0; prefer the installed package's .d.ts files whenever
the application's package version differs.
First-Step Triage
Before implementing or answering in detail, identify:
- Package mode: npm package (
@zoom/videosdk) or CDN.
- Framework: vanilla JavaScript/TypeScript, React, Vue, Svelte, Angular, or other.
- Target task: shortest runnable session, specific feature, layout, or debugging.
- Runtime constraints: desktop/mobile browser, HTTPS/dev server, COOP/COEP/SAB
requirements, user gesture requirements.
Suggested repo checks:
rg -n "@zoom/videosdk|ZoomVideo|WebVideoSDK|video-player-container|attachVideo" .
rg -n "\"react\"|\"vue\"|\"svelte\"|\"@angular/core\"|vite|next|nuxt|angular" package.json . -g 'package.json' -g 'vite.config.*' -g 'angular.json'
Read Routing
Always read only what is needed for the user's task.
Shortest Runnable Session
Read these first:
references/get_started.md
references/auth.md
references/sessions.md
references/video/video.md
references/audio/audio.md
references/screen-sharing/share.md
references/handle_events.md
Use this route when the user wants to join a session, start audio, start video,
see/hear remote users, start screen share, or receive screen share.
Framework Integration
Read references/frameworks.md first, then read framework-specific guidance only
when the current project uses that framework:
- React:
references/framework-integration/react.md
- Next.js:
references/framework-integration/nextjs.md
- Vue:
references/framework-integration/vue.md
- Nuxt:
references/framework-integration/nuxtjs.md
- Svelte:
references/framework-integration/svelte.md
- Angular:
references/framework-integration/angular.md
Keep framework code idiomatic to the existing project. Do not force a new state
management library or UI kit.
Events and State Synchronization
Read:
references/handle_events.md
Use for participant list sync, mid-session join, reconnecting users,
isInFailover, connection state, audio/video/share events, and stale UI.
Troubleshooting
Read:
troubleshooting/common-issues.md
references/error-codes.md
references/features/quality.md when preparing logs or reporting issues to Zoom.
references/browser-support.md when behavior is browser/platform-specific.
Use rejected Promise error.errorCode first, then match known common issues.
Feature-Specific Routing
| User asks about |
Read |
| Audio, devices, high bitrate, original sound, noise suppression |
references/audio/audio.md, references/audio/audio-advanced.md, references/audio/audio-sound-options.md, references/audio/audio-best-practices.md |
| Video rendering, gallery view, speaker view, 1:1 layout, active speaker |
references/video/video.md, references/video/video-best-practices.md |
| HD video, 720p/1080p, camera support |
references/video/video-hd.md, references/browser-support.md |
| Virtual background, PTZ, PiP, second camera |
references/video/video-advanced.md, references/video/video-camera-controls.md, references/video/video-picture-in-picture.md |
| Screen sharing, receive share, multiple shares, share layout |
references/screen-sharing/share.md |
| Annotation or share audio/system audio |
references/screen-sharing/share-annotation.md, references/screen-sharing/share-browser-options.md |
| Command channel or custom in-session control messages |
references/features/command-channel.md |
| Chat |
references/chat/chat.md, references/chat/chat-send-files.md |
| Recording |
references/features/recording.md |
| Subsessions, breakout rooms, waiting-room-like flows |
references/features/subsessions.md, troubleshooting/common-issues.md |
| Live transcription, translation, captions |
references/features/transcription-translation.md |
| PSTN/SIP phone call |
references/features/pstn.md, references/features/sip.md |
| Whiteboard |
references/features/whiteboard.md |
| Preview microphone/camera before session |
references/features/preview.md |
| Live stream / RTMP |
references/features/live-stream.md, references/features/incoming-live-stream.md |
| Broadcast streaming |
references/features/broadcast.md |
| Raw audio, video, or share data |
references/raw-data/raw-data.md, references/raw-data/raw-data-audio.md, references/raw-data/raw-data-video.md, references/raw-data/raw-data-share.md |
| Browser support, SAB, CSP, COOP/COEP |
references/browser-support.md |
Shortest Runnable Implementation
Use this as the default target when the user asks for a working integration.
Adapt it to the detected framework.
Install SDK
NPM:
npm install @zoom/videosdk
CDN:
<script src="https://source.zoom.us/videosdk/zoom-video-#.#.#.min.js"></script>
Prefer npm in modern frontend projects. Use CDN only when the existing app is
script-based or explicitly asks for CDN. With CDN, access the SDK from
window.WebVideoSDK.default.
Generate JWT on Server
Never expose the Video SDK secret in client code.
Required JWT claims:
{
app_key: process.env.ZOOM_SDK_KEY,
tpc: sessionName,
role_type: 0, // 0 participant, 1 host/co-host
version: 1,
iat,
exp,
}
Important claims:
app_key: Video SDK key.
role_type: 1 host/co-host, 0 participant. Must be a number.
tpc: session name, max 200 chars, must match client.join(topic, ...).
user_key: stable user/customer identifier for auditing.
session_key: stable session identifier; all attendees must use the same
value if host sets it.
telemetry_tracking_id: useful when reporting Web SDK issues to Zoom.
video_webrtc_mode / audio_webrtc_mode: JWT-level WebRTC mode hints, not
client.init() options.
Reference: references/auth.md.
Minimal HTML/CSS
video-player elements returned by attachVideo() must be appended inside a
video-player-container. Give the container and players dimensions.
The SDK owns the rendering surface of video-player and video-player-container.
Avoid setting an opaque background / background-color / background-image
directly on video-player, video-player-container, or an element nested between
them while video is showing — a full-area opaque background on these elements can
paint over the rendered video and make it disappear, depending on the runtime.
Overlaying UI on top of the video (name tags, mic/status badges, controls) is
fine: add it as a separately positioned child element, not as a background on the
player/container. For a placeholder/letterbox color shown before video attaches,
prefer an outer wrapper behind the container, and remove or hide it once video is
attached.
<button id="join">Join</button>
<button id="start-audio">Start audio</button>
<button id="start-video">Start video</button>
<button id="start-share">Start share</button>
<video-player-container class="video-grid"></video-player-container>
<video id="local-share-video" width="1920" height="1080"></video>
<canvas id="local-share-canvas" width="1920" height="1080"></canvas>
<video-player-container class="share-container"></video-player-container>
video-player-container.video-grid,
video-player-container.share-container {
width: 100%;
min-height: 360px;
display: flex !important;
flex-wrap: wrap;
align-content: baseline;
gap: 8px;
}
video-player {
width: 100%;
height: auto;
flex: 0 0 50%;
aspect-ratio: 16 / 9;
}
#local-share-video,
#local-share-canvas {
width: 100%;
height: auto;
}
Minimal Client Flow
import ZoomVideo, { VideoQuality } from "@zoom/videosdk";
const client = ZoomVideo.createClient();
let stream;
let activeShareUserId = null;
let startingAudio = false;
let startingVideo = false;
let startingShare = false;
async function joinSession({ topic, token, userName, password }) {
const support = ZoomVideo.checkSystemRequirements();
if (!support.audio || !support.video) {
throw new Error("Browser does not support required Video SDK features.");
}
await client.init("en-US", "Global", { patchJsMedia: true });
await client.join(topic, token, userName, password);
stream = client.getMediaStream();
bindEvents();
await renderExistingVideos();
await renderExistingShare();
}
async function startAudio() {
if (startingAudio) return;
startingAudio = true;
try {
await stream.startAudio();
} finally {
startingAudio = false;
}
}
async function startVideo() {
if (startingVideo) return;
startingVideo = true;
try {
await stream.startVideo();
const userId = client.getCurrentUserInfo().userId;
await attachUserVideo(userId);
} finally {
startingVideo = false;
}
}
async function startShare() {
if (startingShare) return;
startingShare = true;
try {
if (stream.isStartShareScreenWithVideoElement()) {
await stream.startShareScreen(document.querySelector("#local-share-video"));
} else {
await stream.startShareScreen(document.querySelector("#local-share-canvas"));
}
} finally {
startingShare = false;
}
}
function bindEvents() {
client.on("peer-video-state-change", async ({ action, userId }) => {
if (action === "Start") {
await attachUserVideo(userId);
} else if (action === "Stop") {
removeDetachedElements(await stream.detachVideo(userId));
}
});
client.on("active-share-change", async ({ state, userId }) => {
if (state === "Active") {
await attachShare(userId);
} else if (state === "Inactive") {
removeDetachedElements(await stream.detachShareView(userId));
activeShareUserId = null;
}
});
client.on("user-updated", (users) => {
users.forEach((user) => {
if (user.isInFailover) {
// Keep the user visible, but show reconnecting/unstable status.
}
});
});
// Always handle connection-change: it is the source of truth for session
// state. Without it the UI can keep showing "in session" after the user has
// actually disconnected, which desyncs your app from reality.
client.on("connection-change", (payload) => {
if (payload.state === "Connected") {
// Session is live; clear any reconnecting/closed UI.
} else if (payload.state === "Reconnecting") {
// Lost connection, SDK is retrying. Show a reconnecting banner; do not
// tear down session UI yet.
} else if (payload.state === "Closed") {
// Session ended (host ended it, or the user was removed). Tear down
// session UI and route the user out. payload.reason explains why.
} else if (payload.state === "Fail") {
// Join/reconnect failed permanently. Surface an error and leave.
console.error("Connection failed", payload.errorCode, payload.reason);
}
});
// Always handle active-media-failed: media can fail after starting (permission
// reset, device taken by another app, stream interrupted). Turn payload.code
// into a user-facing recovery hint instead of failing silently.
client.on("active-media-failed", (payload) => {
// payload.code (ActiveMediaFailedCode) + payload.message. Map the code to a
// suggested action and show it to the user (grant permission, click the
// page to resume, refresh, etc.). See references/handle_events.md and
// references/error-codes.md for the full code-to-action table.
console.error("active-media-failed", payload.code, payload.message);
});
}
async function renderExistingVideos() {
for (const user of client.getAllUser()) {
if (user.bVideoOn) {
await attachUserVideo(user.userId);
}
}
}
async function attachUserVideo(userId) {
const video = await stream.attachVideo(userId, VideoQuality.Video_360P);
document.querySelector("video-player-container.video-grid").appendChild(video);
}
async function renderExistingShare() {
const sharingUser = client.getAllUser().find((user) => user.sharerOn);
if (sharingUser) {
await attachShare(sharingUser.userId);
}
}
async function attachShare(userId) {
if (activeShareUserId && activeShareUserId !== userId) {
removeDetachedElements(await stream.detachShareView(activeShareUserId));
}
const shareView = await stream.attachShareView(userId);
document.querySelector("video-player-container.share-container").appendChild(shareView);
activeShareUserId = userId;
}
function removeDetachedElements(detached) {
if (Array.isArray(detached)) {
detached.forEach((element) => element.remove());
} else if (detached) {
detached.remove();
}
}
Event Model Guidance
Use SDK events as state change signals, then reconcile with SDK getters when
needed.
Key events:
connection-change: join failure, reconnecting, closed, connected.
user-added, user-removed, user-updated: participant list and failover.
current-audio-change: local or remote audio state changes.
peer-video-state-change: render/detach remote video.
active-share-change: current active share view.
peer-share-state-change: multiple-share flows.
device-change and device-permission-change: device picker and permission UI.
active-media-failed: media failure requiring user intervention.
Always bind these two — they are not optional, even for a minimal integration:
connection-change is the source of truth for session lifecycle. Without it
the app can keep showing "in session" after the user has actually
disconnected, desyncing the UI from reality. Handle every state (Connected,
Reconnecting, Closed, Fail), not just failure: show a reconnecting state
on Reconnecting, and route the user out on Closed/Fail.
active-media-failed fires when media fails after it started (permission
reset, device taken by another app, interrupted stream, WebGL/WASM issues).
Map payload.code to a concrete recovery hint for the user (grant permission,
click the page to resume, refresh) instead of failing silently. The full
code-to-action table is in references/handle_events.md.
Important: a disconnected user may remain in the session briefly because of
server heartbeat/failover handling. Use user.isInFailover to show unstable or
reconnecting UI instead of assuming the user has cleanly left.
Feature Guidance
Command Channel
Suggest command channel for low-frequency custom in-session controls such as
reactions, layout hints, control messages, and app-specific state updates.
Constraints:
- Send strings only; JSON must be
JSON.stringify(...).
- Maximum message size is 512 characters.
- Rate limit is 2 commands per second per session by default.
- Not designed for high-frequency reliable N-to-N broadcast. Use a dedicated
signaling service for that.
Audio
Start audio from a user gesture when possible. Use loading state to prevent
duplicate startAudio() calls. Read advanced docs for high bitrate, original
sound, stereo, background noise suppression, and device handling.
Video Layouts
Use video-player-container as the SDK render host, and build app layout around
returned video-player elements. For gallery, speaker, 1:1, and share-combined
layouts, keep SDK attachment logic separate from CSS/layout state.
Keep the player and container transparent: put name tags, badges, and controls in
positioned overlay children, never as a full-area opaque background on
video-player / video-player-container, which can hide the rendered video.
Reuse the video-player-container; do not destroy and recreate it. The container
holds the shared rendering surface for all videos under it, and the browser caps
how many such surfaces can exist — repeatedly tearing the container down and
recreating it churns that surface and can exhaust the limit, after which video
stops rendering entirely. Mount one video-player-container per render host and
keep it for the session lifetime: do not unmount/remount it on route changes, tab
switches, or layout toggles. In React/Vue, do not place it behind conditional
rendering that destroys it — keep it mounted and toggle visibility with CSS
(display: none / visibility: hidden) instead. Individual video-player
elements may be freely created and removed via attachVideo() / detachVideo();
only the container must stay stable.
Screen Share
Use stream.isStartShareScreenWithVideoElement() to choose video vs canvas when
starting local share. Use attachShareView() / detachShareView() to receive
remote shares. For simultaneous shares, read references/screen-sharing/share.md before
implementing.
Preview
Use preview APIs for pre-session camera and microphone testing. Do not join a
session only to test devices.
Troubleshooting Workflow
- Capture the rejected Promise object and inspect
error.errorCode,
error.type, and error.reason.
- Match
error.errorCode against references/error-codes.md.
- Check
troubleshooting/common-issues.md for known symptoms and fixes.
- Verify the relevant official doc in
references/.
- For browser-specific behavior, check
references/browser-support.md.
- For issues requiring Zoom investigation, follow
references/features/quality.md and
include telemetry tracking ID, SDK version, browser, OS, session details, and
reproducible steps.
Common implementation checks:
- JWT
tpc matches client.join(topic, ...).
- JWT
exp is between 30 minutes and 48 hours after iat.
- Video SDK secret is never exposed in frontend code.
client.init() completed before client.join().
- Media operations are called after join and guarded against duplicate clicks.
video-player has dimensions through CSS and is inside video-player-container.
video-player / video-player-container have no full-area opaque background;
overlays are positioned children, not backgrounds (a covering background hides
video).
video-player-container is mounted once and reused for the session, not
destroyed/recreated on route, tab, or layout changes (recreating it can exhaust
the browser's rendering-surface limit and stop video from rendering).
connection-change is bound and handles Reconnecting/Closed/Fail, not
just the happy path (otherwise the UI desyncs when the session drops).
active-media-failed is bound and surfaces a user-facing recovery hint from
payload.code.
- Remote videos are rendered from both
peer-video-state-change and a post-join
client.getAllUser() reconciliation pass.
- Screen share receive uses
attachShareView() / detachShareView().
- Browser support is checked before exposing unsupported features.
Best Practices
- Generate JWT on a secure backend.
- Keep session lifecycle, media controls, event listeners, and DOM rendering
separated in code.
- Bind core events before or immediately after join, then reconcile with getters.
- Use loading/in-flight flags for
join, startAudio, startVideo,
startShareScreen, recording start/stop, and other async operations.
- Clean up event listeners and detached media elements on leave/unmount.
- Avoid hard-coding HD, SharedArrayBuffer, or WebRTC assumptions; check browser
support and feature docs.
- Prefer concise, project-specific implementation guidance over dumping all SDK
APIs.
Reference Index
For the full list of reference files and when to read each, use the routing
tables above: the "Read Routing" section for core/session/event/troubleshooting
docs, the "Feature-Specific Routing" table for per-feature docs, and the
"Framework Integration" section for framework docs.
For exact API names, signatures, enums, event payloads, and declaration source
locations, use references/type-definitions-json.md.
Repo-Local Appendices
Read these only when they add detail not covered by the official build path:
- Operations and maintenance:
RUNBOOK.md, MAINTENANCE.md
- Architecture:
concepts/sdk-architecture-pattern.md, concepts/singleton-hierarchy.md
- Session implementation:
examples/session-join-pattern.md, examples/event-handling.md
- Media implementation:
examples/audio.md, examples/video-rendering.md, examples/screen-share.md
- Feature examples:
examples/chat.md, examples/command-channel.md, examples/recording.md, examples/transcription.md
- Framework examples:
examples/react-hooks.md, examples/framework-integrations.md
- Consolidated legacy references:
references/web.md, references/web-reference.md, references/audio.md, references/video.md, references/screen-share.md, references/features.md, references/advanced.md, references/events-reference.md, references/common-issues.md
- Extended framework references:
references/react.md, references/vue.md, references/angular.md, references/svelte.md
- Reviewed 2.4.5 sample map:
references/sample-app-2.4.5.md
1---2name: video-sdk-web3description: Build and debug browser-based Zoom Video SDK for Web integrations using @zoom/videosdk. Use for custom video sessions, joining or leaving sessions, JWT auth, audio/video and video-player rendering, screen sharing, chat and file transfer, command channel, recording, subsessions, transcription and translation, RTMS, live or broadcast streaming, raw data, whiteboard, PSTN/SIP, preview devices, browser support, SDK events, exact API/type lookup, or SDK errors. This is for custom Video SDK sessions, not Zoom Meeting SDK embedded meetings.4---56# Zoom Video SDK Web78This skill helps developers integrate Zoom Video SDK for Web into an existing9frontend application. Optimize for working code first, then add feature-specific10behavior and troubleshooting.1112The primary build guidance and nested references are synchronized from the13official `zoom/videosdk-web` repository at SDK version `2.5.0`. Repo-local14concepts and examples complement that upstream material rather than override it.1516Use repo-local official docs under `references/` as the primary source of truth.17Use `troubleshooting/` for symptom-driven debugging.1819## Scope2021Use this skill for:2223- Browser integrations using `@zoom/videosdk`.24- Custom session UI: audio, video, screen share, chat, command channel,25 recording, subsessions, transcription/translation, PSTN/SIP, whiteboard,26 preview devices, and quality reporting.27- Debugging rejected Promises, SDK error codes, browser support, rendering28 issues, and event/lifecycle bugs.2930Do not use this skill for:3132- Joining or embedding regular Zoom Meetings or Webinars with Meeting SDK.33- Generic WebRTC advice that bypasses Video SDK APIs.34- Server-side Video SDK APIs except when generating the client JWT.3536## Operating Principles3738**Never invent SDK API surface.** Do not guess, recall, or "reconstruct" method39names, event strings, enum values, option fields, or payload shapes from memory.40The SDK surface changes across versions and plausible-looking APIs are easy to41hallucinate. Before writing any SDK call, event name, or enum, confirm it exists42in the bundled type definitions (`node_modules/@zoom/videosdk/dist/types/`). If a43symbol cannot be verified there, say so explicitly and check the types or ask —44do not emit a plausible guess. This rule overrides convenience and speed.45461. Start with the shortest runnable path for the developer's current project.472. Detect the current framework from the repo before giving framework-specific48 code.493. Read the relevant docs before making non-trivial claims or changes.504. Prefer official `references/` content over memory.515. Do not provide large API inventories in the answer unless the user asks.526. Treat most SDK operations as async Promise-returning operations; prevent53 duplicate in-flight calls in UI.547. Use `error.errorCode` for actionable troubleshooting.5556Useful type entry points (paths relative to your project root, after57`npm install @zoom/videosdk`):5859- Session/client: `node_modules/@zoom/videosdk/dist/types/videoclient.d.ts`60- Events: `node_modules/@zoom/videosdk/dist/types/event-callback.d.ts`61- Audio / video / screen share: `node_modules/@zoom/videosdk/dist/types/media.d.ts`62- Chat: `node_modules/@zoom/videosdk/dist/types/chat.d.ts`63- Command channel: `node_modules/@zoom/videosdk/dist/types/command.d.ts`64- Recording: `node_modules/@zoom/videosdk/dist/types/recording.d.ts`65- Transcription: `node_modules/@zoom/videosdk/dist/types/live-transcription.d.ts`66- Error codes: `node_modules/@zoom/videosdk/dist/types/exception-code.d.ts`6768When the package is not installed, or when an AI-readable declaration inventory69is more efficient, read `references/type-definitions-json.md` and then only the70matching file under `references/types-json/`. These generated files describe71`@zoom/videosdk` `2.5.0`; prefer the installed package's `.d.ts` files whenever72the application's package version differs.7374## First-Step Triage7576Before implementing or answering in detail, identify:7778- Package mode: npm package (`@zoom/videosdk`) or CDN.79- Framework: vanilla JavaScript/TypeScript, React, Vue, Svelte, Angular, or other.80- Target task: shortest runnable session, specific feature, layout, or debugging.81- Runtime constraints: desktop/mobile browser, HTTPS/dev server, COOP/COEP/SAB82 requirements, user gesture requirements.8384Suggested repo checks:8586```bash87rg -n "@zoom/videosdk|ZoomVideo|WebVideoSDK|video-player-container|attachVideo" .88rg -n "\"react\"|\"vue\"|\"svelte\"|\"@angular/core\"|vite|next|nuxt|angular" package.json . -g 'package.json' -g 'vite.config.*' -g 'angular.json'89```9091## Read Routing9293Always read only what is needed for the user's task.9495### Shortest Runnable Session9697Read these first:9899- `references/get_started.md`100- `references/auth.md`101- `references/sessions.md`102- `references/video/video.md`103- `references/audio/audio.md`104- `references/screen-sharing/share.md`105- `references/handle_events.md`106107Use this route when the user wants to join a session, start audio, start video,108see/hear remote users, start screen share, or receive screen share.109110### Framework Integration111112Read `references/frameworks.md` first, then read framework-specific guidance only113when the current project uses that framework:114115- React: `references/framework-integration/react.md`116- Next.js: `references/framework-integration/nextjs.md`117- Vue: `references/framework-integration/vue.md`118- Nuxt: `references/framework-integration/nuxtjs.md`119- Svelte: `references/framework-integration/svelte.md`120- Angular: `references/framework-integration/angular.md`121122Keep framework code idiomatic to the existing project. Do not force a new state123management library or UI kit.124125### Events and State Synchronization126127Read:128129- `references/handle_events.md`130131Use for participant list sync, mid-session join, reconnecting users,132`isInFailover`, connection state, audio/video/share events, and stale UI.133134### Troubleshooting135136Read:137138- `troubleshooting/common-issues.md`139- `references/error-codes.md`140- `references/features/quality.md` when preparing logs or reporting issues to Zoom.141- `references/browser-support.md` when behavior is browser/platform-specific.142143Use rejected Promise `error.errorCode` first, then match known common issues.144145### Feature-Specific Routing146147| User asks about | Read |148| --- | --- |149| Audio, devices, high bitrate, original sound, noise suppression | `references/audio/audio.md`, `references/audio/audio-advanced.md`, `references/audio/audio-sound-options.md`, `references/audio/audio-best-practices.md` |150| Video rendering, gallery view, speaker view, 1:1 layout, active speaker | `references/video/video.md`, `references/video/video-best-practices.md` |151| HD video, 720p/1080p, camera support | `references/video/video-hd.md`, `references/browser-support.md` |152| Virtual background, PTZ, PiP, second camera | `references/video/video-advanced.md`, `references/video/video-camera-controls.md`, `references/video/video-picture-in-picture.md` |153| Screen sharing, receive share, multiple shares, share layout | `references/screen-sharing/share.md` |154| Annotation or share audio/system audio | `references/screen-sharing/share-annotation.md`, `references/screen-sharing/share-browser-options.md` |155| Command channel or custom in-session control messages | `references/features/command-channel.md` |156| Chat | `references/chat/chat.md`, `references/chat/chat-send-files.md` |157| Recording | `references/features/recording.md` |158| Subsessions, breakout rooms, waiting-room-like flows | `references/features/subsessions.md`, `troubleshooting/common-issues.md` |159| Live transcription, translation, captions | `references/features/transcription-translation.md` |160| PSTN/SIP phone call | `references/features/pstn.md`, `references/features/sip.md` |161| Whiteboard | `references/features/whiteboard.md` |162| Preview microphone/camera before session | `references/features/preview.md` |163| Live stream / RTMP | `references/features/live-stream.md`, `references/features/incoming-live-stream.md` |164| Broadcast streaming | `references/features/broadcast.md` |165| Raw audio, video, or share data | `references/raw-data/raw-data.md`, `references/raw-data/raw-data-audio.md`, `references/raw-data/raw-data-video.md`, `references/raw-data/raw-data-share.md` |166| Browser support, SAB, CSP, COOP/COEP | `references/browser-support.md` |167168## Shortest Runnable Implementation169170Use this as the default target when the user asks for a working integration.171Adapt it to the detected framework.172173### Install SDK174175NPM:176177```bash178npm install @zoom/videosdk179```180181CDN:182183```html184<script src="https://source.zoom.us/videosdk/zoom-video-#.#.#.min.js"></script>185```186187Prefer npm in modern frontend projects. Use CDN only when the existing app is188script-based or explicitly asks for CDN. With CDN, access the SDK from189`window.WebVideoSDK.default`.190191### Generate JWT on Server192193Never expose the Video SDK secret in client code.194195Required JWT claims:196197```javascript198{199 app_key: process.env.ZOOM_SDK_KEY,200 tpc: sessionName,201 role_type: 0, // 0 participant, 1 host/co-host202 version: 1,203 iat,204 exp,205}206```207208Important claims:209210- `app_key`: Video SDK key.211- `role_type`: `1` host/co-host, `0` participant. Must be a number.212- `tpc`: session name, max 200 chars, must match `client.join(topic, ...)`.213- `user_key`: stable user/customer identifier for auditing.214- `session_key`: stable session identifier; all attendees must use the same215 value if host sets it.216- `telemetry_tracking_id`: useful when reporting Web SDK issues to Zoom.217- `video_webrtc_mode` / `audio_webrtc_mode`: JWT-level WebRTC mode hints, not218 `client.init()` options.219220Reference: `references/auth.md`.221222### Minimal HTML/CSS223224`video-player` elements returned by `attachVideo()` must be appended inside a225`video-player-container`. Give the container and players dimensions.226227The SDK owns the rendering surface of `video-player` and `video-player-container`.228Avoid setting an opaque `background` / `background-color` / `background-image`229directly on `video-player`, `video-player-container`, or an element nested between230them while video is showing — a full-area opaque background on these elements can231paint over the rendered video and make it disappear, depending on the runtime.232Overlaying UI on top of the video (name tags, mic/status badges, controls) is233fine: add it as a separately positioned child element, not as a background on the234player/container. For a placeholder/letterbox color shown before video attaches,235prefer an outer wrapper behind the container, and remove or hide it once video is236attached.237238```html239<button id="join">Join</button>240<button id="start-audio">Start audio</button>241<button id="start-video">Start video</button>242<button id="start-share">Start share</button>243244<video-player-container class="video-grid"></video-player-container>245246<video id="local-share-video" width="1920" height="1080"></video>247<canvas id="local-share-canvas" width="1920" height="1080"></canvas>248<video-player-container class="share-container"></video-player-container>249```250251```css252video-player-container.video-grid,253video-player-container.share-container {254 width: 100%;255 min-height: 360px;256 display: flex !important;257 flex-wrap: wrap;258 align-content: baseline;259 gap: 8px;260}261262video-player {263 width: 100%;264 height: auto;265 flex: 0 0 50%;266 aspect-ratio: 16 / 9;267}268269#local-share-video,270#local-share-canvas {271 width: 100%;272 height: auto;273}274```275276### Minimal Client Flow277278```javascript279import ZoomVideo, { VideoQuality } from "@zoom/videosdk";280281const client = ZoomVideo.createClient();282let stream;283let activeShareUserId = null;284let startingAudio = false;285let startingVideo = false;286let startingShare = false;287288async function joinSession({ topic, token, userName, password }) {289 const support = ZoomVideo.checkSystemRequirements();290 if (!support.audio || !support.video) {291 throw new Error("Browser does not support required Video SDK features.");292 }293294 await client.init("en-US", "Global", { patchJsMedia: true });295 await client.join(topic, token, userName, password);296297 stream = client.getMediaStream();298 bindEvents();299 await renderExistingVideos();300 await renderExistingShare();301}302303async function startAudio() {304 if (startingAudio) return;305 startingAudio = true;306 try {307 await stream.startAudio();308 } finally {309 startingAudio = false;310 }311}312313async function startVideo() {314 if (startingVideo) return;315 startingVideo = true;316 try {317 await stream.startVideo();318 const userId = client.getCurrentUserInfo().userId;319 await attachUserVideo(userId);320 } finally {321 startingVideo = false;322 }323}324325async function startShare() {326 if (startingShare) return;327 startingShare = true;328 try {329 if (stream.isStartShareScreenWithVideoElement()) {330 await stream.startShareScreen(document.querySelector("#local-share-video"));331 } else {332 await stream.startShareScreen(document.querySelector("#local-share-canvas"));333 }334 } finally {335 startingShare = false;336 }337}338339function bindEvents() {340 client.on("peer-video-state-change", async ({ action, userId }) => {341 if (action === "Start") {342 await attachUserVideo(userId);343 } else if (action === "Stop") {344 removeDetachedElements(await stream.detachVideo(userId));345 }346 });347348 client.on("active-share-change", async ({ state, userId }) => {349 if (state === "Active") {350 await attachShare(userId);351 } else if (state === "Inactive") {352 removeDetachedElements(await stream.detachShareView(userId));353 activeShareUserId = null;354 }355 });356357 client.on("user-updated", (users) => {358 users.forEach((user) => {359 if (user.isInFailover) {360 // Keep the user visible, but show reconnecting/unstable status.361 }362 });363 });364365 // Always handle connection-change: it is the source of truth for session366 // state. Without it the UI can keep showing "in session" after the user has367 // actually disconnected, which desyncs your app from reality.368 client.on("connection-change", (payload) => {369 if (payload.state === "Connected") {370 // Session is live; clear any reconnecting/closed UI.371 } else if (payload.state === "Reconnecting") {372 // Lost connection, SDK is retrying. Show a reconnecting banner; do not373 // tear down session UI yet.374 } else if (payload.state === "Closed") {375 // Session ended (host ended it, or the user was removed). Tear down376 // session UI and route the user out. payload.reason explains why.377 } else if (payload.state === "Fail") {378 // Join/reconnect failed permanently. Surface an error and leave.379 console.error("Connection failed", payload.errorCode, payload.reason);380 }381 });382383 // Always handle active-media-failed: media can fail after starting (permission384 // reset, device taken by another app, stream interrupted). Turn payload.code385 // into a user-facing recovery hint instead of failing silently.386 client.on("active-media-failed", (payload) => {387 // payload.code (ActiveMediaFailedCode) + payload.message. Map the code to a388 // suggested action and show it to the user (grant permission, click the389 // page to resume, refresh, etc.). See references/handle_events.md and390 // references/error-codes.md for the full code-to-action table.391 console.error("active-media-failed", payload.code, payload.message);392 });393}394395async function renderExistingVideos() {396 for (const user of client.getAllUser()) {397 if (user.bVideoOn) {398 await attachUserVideo(user.userId);399 }400 }401}402403async function attachUserVideo(userId) {404 const video = await stream.attachVideo(userId, VideoQuality.Video_360P);405 document.querySelector("video-player-container.video-grid").appendChild(video);406}407408async function renderExistingShare() {409 const sharingUser = client.getAllUser().find((user) => user.sharerOn);410 if (sharingUser) {411 await attachShare(sharingUser.userId);412 }413}414415async function attachShare(userId) {416 if (activeShareUserId && activeShareUserId !== userId) {417 removeDetachedElements(await stream.detachShareView(activeShareUserId));418 }419420 const shareView = await stream.attachShareView(userId);421 document.querySelector("video-player-container.share-container").appendChild(shareView);422 activeShareUserId = userId;423}424425function removeDetachedElements(detached) {426 if (Array.isArray(detached)) {427 detached.forEach((element) => element.remove());428 } else if (detached) {429 detached.remove();430 }431}432```433434## Event Model Guidance435436Use SDK events as state change signals, then reconcile with SDK getters when437needed.438439Key events:440441- `connection-change`: join failure, reconnecting, closed, connected.442- `user-added`, `user-removed`, `user-updated`: participant list and failover.443- `current-audio-change`: local or remote audio state changes.444- `peer-video-state-change`: render/detach remote video.445- `active-share-change`: current active share view.446- `peer-share-state-change`: multiple-share flows.447- `device-change` and `device-permission-change`: device picker and permission UI.448- `active-media-failed`: media failure requiring user intervention.449450Always bind these two — they are not optional, even for a minimal integration:451452- `connection-change` is the source of truth for session lifecycle. Without it453 the app can keep showing "in session" after the user has actually454 disconnected, desyncing the UI from reality. Handle every state (`Connected`,455 `Reconnecting`, `Closed`, `Fail`), not just failure: show a reconnecting state456 on `Reconnecting`, and route the user out on `Closed`/`Fail`.457- `active-media-failed` fires when media fails *after* it started (permission458 reset, device taken by another app, interrupted stream, WebGL/WASM issues).459 Map `payload.code` to a concrete recovery hint for the user (grant permission,460 click the page to resume, refresh) instead of failing silently. The full461 code-to-action table is in `references/handle_events.md`.462463Important: a disconnected user may remain in the session briefly because of464server heartbeat/failover handling. Use `user.isInFailover` to show unstable or465reconnecting UI instead of assuming the user has cleanly left.466467## Feature Guidance468469### Command Channel470471Suggest command channel for low-frequency custom in-session controls such as472reactions, layout hints, control messages, and app-specific state updates.473474Constraints:475476- Send strings only; JSON must be `JSON.stringify(...)`.477- Maximum message size is 512 characters.478- Rate limit is 2 commands per second per session by default.479- Not designed for high-frequency reliable N-to-N broadcast. Use a dedicated480 signaling service for that.481482### Audio483484Start audio from a user gesture when possible. Use loading state to prevent485duplicate `startAudio()` calls. Read advanced docs for high bitrate, original486sound, stereo, background noise suppression, and device handling.487488### Video Layouts489490Use `video-player-container` as the SDK render host, and build app layout around491returned `video-player` elements. For gallery, speaker, 1:1, and share-combined492layouts, keep SDK attachment logic separate from CSS/layout state.493494Keep the player and container transparent: put name tags, badges, and controls in495positioned overlay children, never as a full-area opaque background on496`video-player` / `video-player-container`, which can hide the rendered video.497498Reuse the `video-player-container`; do not destroy and recreate it. The container499holds the shared rendering surface for all videos under it, and the browser caps500how many such surfaces can exist — repeatedly tearing the container down and501recreating it churns that surface and can exhaust the limit, after which video502stops rendering entirely. Mount one `video-player-container` per render host and503keep it for the session lifetime: do not unmount/remount it on route changes, tab504switches, or layout toggles. In React/Vue, do not place it behind conditional505rendering that destroys it — keep it mounted and toggle visibility with CSS506(`display: none` / `visibility: hidden`) instead. Individual `video-player`507elements may be freely created and removed via `attachVideo()` / `detachVideo()`;508only the container must stay stable.509510### Screen Share511512Use `stream.isStartShareScreenWithVideoElement()` to choose video vs canvas when513starting local share. Use `attachShareView()` / `detachShareView()` to receive514remote shares. For simultaneous shares, read `references/screen-sharing/share.md` before515implementing.516517### Preview518519Use preview APIs for pre-session camera and microphone testing. Do not join a520session only to test devices.521522## Troubleshooting Workflow5235241. Capture the rejected Promise object and inspect `error.errorCode`,525 `error.type`, and `error.reason`.5262. Match `error.errorCode` against `references/error-codes.md`.5273. Check `troubleshooting/common-issues.md` for known symptoms and fixes.5284. Verify the relevant official doc in `references/`.5295. For browser-specific behavior, check `references/browser-support.md`.5306. For issues requiring Zoom investigation, follow `references/features/quality.md` and531 include telemetry tracking ID, SDK version, browser, OS, session details, and532 reproducible steps.533534Common implementation checks:535536- JWT `tpc` matches `client.join(topic, ...)`.537- JWT `exp` is between 30 minutes and 48 hours after `iat`.538- Video SDK secret is never exposed in frontend code.539- `client.init()` completed before `client.join()`.540- Media operations are called after join and guarded against duplicate clicks.541- `video-player` has dimensions through CSS and is inside `video-player-container`.542- `video-player` / `video-player-container` have no full-area opaque background;543 overlays are positioned children, not backgrounds (a covering background hides544 video).545- `video-player-container` is mounted once and reused for the session, not546 destroyed/recreated on route, tab, or layout changes (recreating it can exhaust547 the browser's rendering-surface limit and stop video from rendering).548- `connection-change` is bound and handles `Reconnecting`/`Closed`/`Fail`, not549 just the happy path (otherwise the UI desyncs when the session drops).550- `active-media-failed` is bound and surfaces a user-facing recovery hint from551 `payload.code`.552- Remote videos are rendered from both `peer-video-state-change` and a post-join553 `client.getAllUser()` reconciliation pass.554- Screen share receive uses `attachShareView()` / `detachShareView()`.555- Browser support is checked before exposing unsupported features.556557## Best Practices558559- Generate JWT on a secure backend.560- Keep session lifecycle, media controls, event listeners, and DOM rendering561 separated in code.562- Bind core events before or immediately after join, then reconcile with getters.563- Use loading/in-flight flags for `join`, `startAudio`, `startVideo`,564 `startShareScreen`, recording start/stop, and other async operations.565- Clean up event listeners and detached media elements on leave/unmount.566- Avoid hard-coding HD, SharedArrayBuffer, or WebRTC assumptions; check browser567 support and feature docs.568- Prefer concise, project-specific implementation guidance over dumping all SDK569 APIs.570571## Reference Index572573For the full list of reference files and when to read each, use the routing574tables above: the "Read Routing" section for core/session/event/troubleshooting575docs, the "Feature-Specific Routing" table for per-feature docs, and the576"Framework Integration" section for framework docs.577578For exact API names, signatures, enums, event payloads, and declaration source579locations, use `references/type-definitions-json.md`.580581## Repo-Local Appendices582583Read these only when they add detail not covered by the official build path:584585- Operations and maintenance: `RUNBOOK.md`, `MAINTENANCE.md`586- Architecture: `concepts/sdk-architecture-pattern.md`, `concepts/singleton-hierarchy.md`587- Session implementation: `examples/session-join-pattern.md`, `examples/event-handling.md`588- Media implementation: `examples/audio.md`, `examples/video-rendering.md`, `examples/screen-share.md`589- Feature examples: `examples/chat.md`, `examples/command-channel.md`, `examples/recording.md`, `examples/transcription.md`590- Framework examples: `examples/react-hooks.md`, `examples/framework-integrations.md`591- Consolidated legacy references: `references/web.md`, `references/web-reference.md`, `references/audio.md`, `references/video.md`, `references/screen-share.md`, `references/features.md`, `references/advanced.md`, `references/events-reference.md`, `references/common-issues.md`592- Extended framework references: `references/react.md`, `references/vue.md`, `references/angular.md`, `references/svelte.md`593- Reviewed 2.4.5 sample map: `references/sample-app-2.4.5.md`