Files
2026-07-13 12:20:06 +08:00

11 KiB

Framework Integrations

Official quickstarts and patterns for popular frameworks.

Next.js (App Router)

Official repo: zoom/videosdk-nextjs-quickstart

Project Structure

app/
├── api/
│   └── signature/
│       └── route.ts      # Server-side JWT generation
├── video/
│   └── page.tsx          # Video call component
├── layout.tsx
└── page.tsx

.env.local
├── ZOOM_SDK_KEY="your-key"
└── ZOOM_SDK_SECRET="your-secret"

Server-Side JWT Generation (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 Component (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] = useState<typeof VideoClient | null>(null);
  const [stream, setStream] = useState<typeof Stream | null>(null);
  const [isJoined, setIsJoined] = useState(false);
  const containerRef = useRef<HTMLDivElement>(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>
  );
}

SSR Considerations

CRITICAL: The Video SDK uses WebAssembly and must run client-side only.

// ❌ 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 });

Next.js (Pages Router)

Branch: pages-router

Server-Side JWT (Pages Router)

// 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 });
}

Client Component (Pages Router)

// 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 />;
}

Vue 3 / Nuxt 3

Official repo: zoom/videosdk-vue-nuxt-quickstart

Composition API Pattern

<!-- components/VideoSession.vue -->
<script setup lang="ts">
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 = ref<typeof VideoClient | null>(null);
const stream = ref<typeof Stream | null>(null);
const isJoined = ref(false);
const videoContainer = ref<HTMLDivElement | null>(null);

// Reactive participants list
const participants = ref<any[]>([]);

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 class="video-session">
    <div v-if="!isJoined">
      <button @click="joinSession">Join Session</button>
    </div>

    <div v-else>
      <div ref="videoContainer" class="video-container" />

      <div class="participants">
        <div v-for="p in participants" :key="p.userId">
          {{ p.displayName }} - {{ p.bVideoOn ? 'Video On' : 'Video Off' }}
        </div>
      </div>

      <div class="controls">
        <button @click="startVideo">Start Video</button>
        <button @click="toggleMute">Toggle Mute</button>
      </div>
    </div>
  </div>
</template>

Nuxt 3 Plugin (Client-Only)

// 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>

Zoom For Government (ZFG)

If using Zoom For Government, you need a separate SDK key from marketplace.zoomgov.com.

Option 1: ZFG-Specific Package Version

{
  "dependencies": {
    "@zoom/videosdk": "1.11.0-zfg"
  }
}
client.init('en-US', 'Global');

Option 2: Custom WebEndpoint

client.init('en-US', 'https://source.zoomgov.com/videosdk/1.11.0/lib', {
  webEndpoint: 'www.zoomgov.com',
});

Official Sample Repositories

Framework Repository Branch
Vanilla JS/TS videosdk-web-sample master
React videosdk-react main
Next.js (App) videosdk-nextjs-quickstart app-router
Next.js (Pages) videosdk-nextjs-quickstart pages-router
Vue/Nuxt videosdk-vue-nuxt-quickstart main
UI Toolkit (React) videosdk-zoom-ui-toolkit-react-sample main
Auth Endpoint videosdk-auth-endpoint-sample main

Common Patterns Across Frameworks

1. Client-Side Only

The SDK must run client-side. All frameworks need to handle this:

Framework Solution
Next.js 'use client' or dynamic(..., { ssr: false })
Nuxt 3 .client.ts plugin or <ClientOnly>
Vue SPA No special handling needed

2. Lifecycle Management

// 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();

3. Event-Driven Video Rendering

// 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);
  }
});