)
Zoom Video SDK Web 框架集成实践Next.js 与 Vue/Nuxt 完整模式knowledge-work-plugins【免费下载链接】knowledge-work-pluginsOpen source repository of plugins primarily intended for knowledge workers to use in Claude Cowork项目地址: https://gitcode.com/GitHub_Trending/kn/knowledge-work-plugins本文基于 framework-integrations.md 展开系统讲解 Zoom Video SDK for Web 在 Next.jsApp Router / Pages Router、Vue 3 / Nuxt 3 及 Zoom For Government 环境下的集成模式。读完后你将掌握服务端 JWT 签名的正确生成方式、各框架仅客户端运行 SDK的标准解法、客户端组件的生命周期管理以及跨框架通用的事件驱动视频渲染模式。一、文档定位与适用背景该文档位于 knowledge-work-plugins 仓库中partner-built/zoom-plugin插件的 Video SDK Web 技能库内与 会话加入模式、视频渲染指南、React Hooks 指南 等示例文档共同构成 SKILL.md 定义的完整文档导航。zoom-plugin 本身是一个面向 Zoom 集成规划、构建与调试的 Claude 插件见 插件 README其/build-zoom-video-sdk-app工作流会路由到这套 video-sdk 参考库。在开始框架集成前会话加入模式 中给出了通用前提条件这些前提对所有框架都适用来自 Zoom Marketplace 的 Video SDK 凭证SDK Key / SDK Secret服务端生成的 JWT 签名客户端绝不能接触 SDK Secret现代浏览器支持Chrome 80、Firefox 75、Safari 14、Edge 80。框架集成文档解决的核心问题是zoom/videosdk基于 WebAssembly 构建只能在浏览器端运行而 Next.js、Nuxt 这类支持服务端渲染SSR的框架会在 Node.js 环境中执行组件代码——如果不做隔离SDK 导入会直接导致 SSR 崩溃。因此每种框架都需要一个服务端只负责发签名、客户端负责跑 SDK的分层方案。二、Next.jsApp Router集成官方快速启动仓库为zoom/videosdk-nextjs-quickstartapp-router分支。2.1 项目结构app/ ├── api/ │ └── signature/ │ └── route.ts # Server-side JWT generation ├── video/ │ └── page.tsx # Video call component ├── layout.tsx └── page.tsx .env.local ├── ZOOM_SDK_KEYyour-key └── ZOOM_SDK_SECRETyour-secret要点凭证通过.env.local注入仅在服务端可用app/api/signature/route.ts是唯一的签名端点app/video/page.tsx是视频通话客户端组件。2.2 服务端 JWT 生成App Router// app/api/signature/route.ts import { NextRequest, NextResponse } from next/server; import KJUR from jsrsasign; export async function POST(request: NextRequest) { const { topic, role } await request.json(); const iat Math.floor(Date.now() / 1000) - 30; const exp iat 60 * 60 * 2; // 2 hours const header { alg: HS256, typ: JWT }; const payload { app_key: process.env.ZOOM_SDK_KEY, tpc: topic, role_type: role || 1, version: 1, iat, exp, }; const signature KJUR.jws.JWS.sign( HS256, JSON.stringify(header), JSON.stringify(payload), process.env.ZOOM_SDK_SECRET! ); return NextResponse.json({ signature }); }对照 会话加入模式 中client.join(topic, signature, userName, password)的参数要求各 JWT 字段含义如下字段取值说明app_keyZOOM_SDK_KEYMarketplace 应用的 SDK Key必须与客户端 SDK 所属应用一致tpc请求体中的topic会话名必须与join()传入的 topic 完全一致role_type默认1角色类型首个以role1加入的用户成为主持人hostversion1签名版本iat当前时间 - 30 秒签发时间前移 30 秒是为容忍服务器与 Zoom 服务端之间的时钟偏差expiat 2 小时过期时间签名过期后join()会报Invalid signature签名算法为 HS256 对称签名密钥即ZOOM_SDK_SECRET——这正是它必须留在服务端的原因。2.3 客户端组件App Router// app/video/page.tsx use client; import { useEffect, useState, useRef } from react; import ZoomVideo, { VideoClient, Stream, VideoQuality } from zoom/videosdk; export default function VideoPage() { const [client, setClient] useStatetypeof VideoClient | null(null); const [stream, setStream] useStatetypeof Stream | null(null); const [isJoined, setIsJoined] useState(false); const containerRef useRefHTMLDivElement(null); useEffect(() { const init async () { const zmClient ZoomVideo.createClient(); await zmClient.init(en-US, Global, { patchJsMedia: true }); setClient(zmClient); }; init(); return () { ZoomVideo.destroyClient(); }; }, []); const joinSession async (topic: string, userName: string) { if (!client) return; // Get signature from API route const res await fetch(/api/signature, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ topic, role: 1 }), }); const { signature } await res.json(); // Join session await client.join(topic, signature, userName); const mediaStream client.getMediaStream(); setStream(mediaStream); setIsJoined(true); }; const startVideo async () { if (!stream || !client) return; await stream.startVideo(); const currentUser client.getCurrentUserInfo(); const element await stream.attachVideo( currentUser.userId, VideoQuality.Video_360P ); containerRef.current?.appendChild(element); }; return ( div {!isJoined ? ( button onClick{() joinSession(my-topic, User)} Join Session /button ) : ( div ref{containerRef} / button onClick{startVideo}Start Video/button / )} /div ); }结合 SDK 架构模式 可以进一步解读几个关键细节init(en-US, Global, { patchJsMedia: true })的第二参数是资产来源可选Global默认source.zoom.us、CDNCloudFront、CNjssdk.zoomus.cn或自托管资产的自定义路径组件状态管理严格对应 SDK 生命周期client在useEffect中创建并init()stream只在join()完成后从client.getMediaStream()获取随后再startVideo()和attachVideo()清理函数中调用ZoomVideo.destroyClient()销毁单例客户端避免热重载或路由切换后残留状态。2.4 SSR 注意事项关键点Video SDK 使用 WebAssembly必须仅在客户端运行。// ❌ WRONG: Importing at top level causes SSR issues import ZoomVideo from zoom/videosdk; // ✅ CORRECT: Dynamic import or use use client directive use client; // Or use dynamic import const ZoomVideo dynamic(() import(zoom/videosdk), { ssr: false });在 App Router 中use client指令本身已足够阻止组件在服务端执行SDK 的实际使用发生在useEffect内只在浏览器触发dynamic(..., { ssr: false })则是更彻底的隔离方式连模块求值也推迟到客户端。三、Next.jsPages Router集成官方快速启动仓库的pages-router分支对应这套模式。3.1 服务端 JWTPages Router与 App Router 版本逻辑完全一致只是路由形式从route.ts的命名导出变为pages/api的默认导出并额外增加了方法校验// pages/api/signature.ts import type { NextApiRequest, NextApiResponse } from next; import KJUR from jsrsasign; export default function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method ! POST) { return res.status(405).json({ error: Method not allowed }); } const { topic, role } req.body; const iat Math.floor(Date.now() / 1000) - 30; const exp iat 60 * 60 * 2; const header { alg: HS256, typ: JWT }; const payload { app_key: process.env.ZOOM_SDK_KEY, tpc: topic, role_type: role || 1, version: 1, iat, exp, }; const signature KJUR.jws.JWS.sign( HS256, JSON.stringify(header), JSON.stringify(payload), process.env.ZOOM_SDK_SECRET! ); res.status(200).json({ signature }); }3.2 客户端组件Pages RouterPages Router 没有use client指令官方模式是页面壳 动态导入页面本身保持纯服务端渲染把含 SDK 的实际组件抽到components/Video.tsx用next/dynamic以ssr: false加载// pages/video.tsx import { useEffect, useState } from react; import dynamic from next/dynamic; // Dynamic import to prevent SSR issues const VideoComponent dynamic(() import(../components/Video), { ssr: false, }); export default function VideoPage() { return VideoComponent /; }components/Video.tsx内部即可按 App Router 客户端组件同款写法使用createClient() → init() → join() → getMediaStream()流程参见 会话加入模式 的完整 React 示例。四、Vue 3 / Nuxt 3 集成官方快速启动仓库为zoom/videosdk-vue-nuxt-quickstartmain分支。4.1 Composition API 模式完整会话组件包含客户端初始化与销毁、参与者列表响应式维护、基于peer-video-state-change的远端视频挂载/卸载、音频静音切换!-- components/VideoSession.vue -- script setup langts import { ref, onMounted, onUnmounted } from vue; import ZoomVideo, { VideoClient, Stream, VideoQuality } from zoom/videosdk; const props defineProps{ topic: string; signature: string; userName: string; }(); const client reftypeof VideoClient | null(null); const stream reftypeof Stream | null(null); const isJoined ref(false); const videoContainer refHTMLDivElement | null(null); // Reactive participants list const participants refany[]([]); onMounted(async () { // Initialize client const zmClient ZoomVideo.createClient(); await zmClient.init(en-US, Global, { patchJsMedia: true }); client.value zmClient; // Set up event listeners zmClient.on(user-added, updateParticipants); zmClient.on(user-removed, updateParticipants); zmClient.on(user-updated, updateParticipants); zmClient.on(peer-video-state-change, handleVideoChange); }); onUnmounted(async () { if (client.value) { await client.value.leave(); ZoomVideo.destroyClient(); } }); const updateParticipants () { if (client.value) { participants.value client.value.getAllUser(); } }; const handleVideoChange async (payload: { action: string; userId: number }) { if (!stream.value || !videoContainer.value) return; if (payload.action Start) { const element await stream.value.attachVideo( payload.userId, VideoQuality.Video_360P ); videoContainer.value.appendChild(element); } else { await stream.value.detachVideo(payload.userId); } }; const joinSession async () { if (!client.value) return; await client.value.join(props.topic, props.signature, props.userName); stream.value client.value.getMediaStream(); isJoined.value true; updateParticipants(); }; const startVideo async () { if (!stream.value || !client.value) return; await stream.value.startVideo(); const currentUser client.value.getCurrentUserInfo(); const element await stream.value.attachVideo( currentUser.userId, VideoQuality.Video_360P ); videoContainer.value?.appendChild(element); }; const toggleMute async () { if (!stream.value) return; const muted stream.value.isAudioMuted(); if (muted) { await stream.value.unmuteAudio(); } else { await stream.value.muteAudio(); } }; /script template div classvideo-session div v-if!isJoined button clickjoinSessionJoin Session/button /div div v-else div refvideoContainer classvideo-container / div classparticipants div v-forp in participants :keyp.userId {{ p.displayName }} - {{ p.bVideoOn ? Video On : Video Off }} /div /div div classcontrols button clickstartVideoStart Video/button button clicktoggleMuteToggle Mute/button /div /div /div /template与 React 版本对比值得注意的框架差异有三处事件监听注册时机Vue 示例在onMounted初始化客户端的同时即注册user-added/user-removed/user-updated/peer-video-state-change监听器利用 ref 闭包在回调中读取最新状态无需像 React 那样按client、stream依赖项拆分多个useEffect清理顺序onUnmounted中先leave()退出会话再destroyClient()销毁单例——这正是 RUNBOOK.md 中Cleanup Upgrade Posture一节要求的退出会话、释放客户端资源的落地参与者状态通过client.getAllUser()同步到响应式数组bVideoOn布尔字段驱动模板渲染。4.2 Nuxt 3 客户端专用插件Nuxt 3 中把 SDK 注入为全局客户端能力用.client.ts后缀确保插件只在浏览器执行// plugins/videosdk.client.ts import ZoomVideo from zoom/videosdk; export default defineNuxtPlugin(() { return { provide: { zoomVideo: ZoomVideo, }, }; });!-- pages/video.vue -- script setup const { $zoomVideo } useNuxtApp(); // Use $zoomVideo.createClient() etc. /script页面内也可直接用ClientOnly组件包裹含 SDK 的模板部分作为另一层 SSR 隔离。五、Zoom For GovernmentZFG如果目标用户是 Zoom For Government 环境需要使用来自 marketplace.zoomgov.com 的独立 SDK Key并且有两条落地路径选项 1使用 ZFG 专属包版本{ dependencies: { zoom/videosdk: 1.11.0-zfg } }client.init(en-US, Global);选项 2自定义 WebEndpoint保持标准包版本但把init()的资产路径与接入点显式指向政府云client.init(en-US, https://source.zoomgov.com/videosdk/1.11.0/lib, { webEndpoint: www.zoomgov.com, });这印证了 SDK 架构模式 中对init(language, dependentAssets, options)的说明第二个参数支持自定义路径ZFG 只是把自托管路径用在了官方政府云资产上。六、官方示例仓库文档汇总了各框架对应的官方示例仓库名称与分支如下可按名称检索官方发布渠道获取框架仓库分支Vanilla JS/TSvideosdk-web-samplemasterReactvideosdk-reactmainNext.js (App)videosdk-nextjs-quickstartapp-routerNext.js (Pages)videosdk-nextjs-quickstartpages-routerVue/Nuxtvideosdk-vue-nuxt-quickstartmainUI Toolkit (React)videosdk-zoom-ui-toolkit-react-samplemainAuth Endpointvideosdk-auth-endpoint-samplemain其中 Auth Endpoint 示例专门演示服务端签名服务的独立实现适合作为本文 JWT 端点的对照参考。七、跨框架通用模式框架集成文档最后总结了所有框架都必须遵守的三条模式这也是框架无关的 Video SDK 契约7.1 仅客户端运行框架解决方案Next.jsuse client或dynamic(..., { ssr: false })Nuxt 3.client.ts插件或ClientOnlyVue SPA无需特殊处理7.2 生命周期管理// Always follow this order: const client ZoomVideo.createClient(); await client.init(...); await client.join(...); const stream client.getMediaStream(); // ONLY after join() // Cleanup on unmount await client.leave(); ZoomVideo.destroyClient();这个顺序在 SKILL.md 中被标记为 SDK Lifecycle (CRITICAL ORDER)并明确警告违反该顺序会导致静默失败。最典型的是getMediaStream()必须在join()完成后调用——在此之前调用会返回undefined而不会抛出任何异常// 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!7.3 事件驱动视频渲染// All frameworks should use this pattern client.on(peer-video-state-change, async ({ action, userId }) { if (action Start) { const el await stream.attachVideo(userId, VideoQuality.Video_360P); container.appendChild(el); } else { await stream.detachVideo(userId); } });远端视频不是拉出来的而是由peer-video-state-change事件推出来的attachVideo()返回一个 VideoPlayer DOM 元素必须手动appendChild到容器。renderVideo()已弃用不要使用见 视频渲染指南。八、深入仓库文档对关键细节的佐证框架集成文档给出的是骨架代码仓库同目录下的其他文档补充了生产化所需的关键细节1. 中会话加入mid-session join的手动补渲染peer-video-state-change只会在你加入之后发生。如果会议里已有人开着摄像头你需要在join()后主动遍历渲染存量参与者会话加入模式 中的renderExistingParticipants()async function renderExistingParticipants() { 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); } } }2. 画质选择与 WebRTC 模式VideoQuality枚举的数值映射为Video_90P(0)、Video_180P(1)、Video_360P(2推荐默认)、Video_720P(3)、Video_1080P(4)。1080P 需要在init()时启用 WebRTC 模式await client.init(en-US, Global, { patchJsMedia: true, webrtc: true, // Required for HD video });启用后可用stream.isSupportHDVideo()检测设备能力、stream.getVideoMaxQuality()获取当前最大画质再决定是否以 720P/1080P 挂载。3. 加入前的能力检查与错误处理SKILL.md 建议在任何框架的客户端初始化前做兼容性检查const compatibility ZoomVideo.checkSystemRequirements(); console.log(Audio:, compatibility.audio, Video:, compatibility.video);而加入失败时的错误分支在框架无关层面已约定好见 会话加入模式 的handleJoinError错误信息含signature表示签名无效需重新签发含Session表示主持人尚未开会含password表示密码错误含Permission表示摄像头/麦克风权限被拒。这些判断逻辑可以直接复用到本文各框架的joinSession中。4. CDN 加载与 HD 视频的部署细节若走 CDN 而非 npm全局对象是WebVideoSDK.default而非ZoomVideo且source.zoom.us可能被网络策略或广告拦截器阻断——SKILL.md 给出的降级策略是白名单放行或在允许的前提下自托管镜像并保持版本同步。追求 HD 性能时还需要在服务器配置 COOP/COEP 响应头Cross-Origin-Opener-Policy: same-origin、Cross-Origin-Embedder-Policy: require-corp以启用 SharedArrayBuffer自 v1.11.2 起该项已是可选项而非硬性要求。5. 排障入口集成出问题时的快速诊断清单在 common-issues.md依次核对生命周期顺序、getMediaStream()时机、peer-video-state-change监听、attachVideo()用法、浏览器权限与版本兼容。更完整的预检流程见 RUNBOOK.md确认集成面 → 确认凭证 → 确认生命周期顺序 → 确认事件状态处理 → 确认清理策略 → 快速探针 → 决策树。九、小结与延伸阅读框架集成的本质是把 Video SDK 的严格生命周期契约createClient → init → join → getMediaStream正确地安放进各框架的执行模型里Next.js 靠use client/dynamic隔离 SSRNuxt 靠.client.ts插件签名永远由服务端 API 路由用 HS256 生成、tpc与join()的 topic 严格一致、iat前移 30 秒防时钟偏差。掌握这些后具体框架差异只剩生命周期钩子的写法不同。深入阅读建议按 SKILL.md 的导航顺序SDK 架构模式 —— 通用五步模式理解它即可实现任何功能会话加入模式 —— JWT 加入会话完整代码视频渲染 ——attachVideo()全部模式React Hooks —— 官方zoom/videosdk-react封装库useSession、useSessionUsers等常见问题 与 RUNBOOK —— 排障清单。【免费下载链接】knowledge-work-pluginsOpen source repository of plugins primarily intended for knowledge workers to use in Claude Cowork项目地址: https://gitcode.com/GitHub_Trending/kn/knowledge-work-plugins创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考