Vue / Nuxt Video Conferencing App with the Zoom Video SDK
The Zoom Video SDK provides video, audio, screen sharing, chat, and more with an easy to use SDK. In this blog we'll showcase adding video chat to a Nuxt web application.
If you're building a Vue application without Nuxt you can skip to Build the Videocall Component section, and use your own backend logic to generate JWTs. You can find the code for this blog on GitHub.
Prerequisites:
- Node LTS & NPM
- A Zoom Video SDK Account
Step 1: Scaffold the application
If you already have a Nuxt project, you can skip this step. To scaffold our application we use Nuxi the Nuxt CLI.
Open a terminal and execute:
npx nuxi@latest init <project-name>
You can use the package manager of your choice.
Step 2: Configuring the project
Install the dependencies
We will install the Zoom Video SDK and jsrsasign a library to generate JWTs. You can use the following command for installation:
npm install @zoom/videosdk jsrsasign
Enable shared array buffers
To leverage the full power of the Zoom Video SDK including features like rendering multiple videos, virtual background, & background noise suppression, we need to enable support for Shared Array Buffers (SAB) in the browser.
Simply, download this file and place it in the public folder of your project as public/coi-serviceworker.js. Later we'll import this file to enable SAB support. You can read more about SAB in our documentation.
Add environment variables
To complete the setup, create a .env file in the root of our project and add the Zoom Video SDK Key and Secret to it. You can find your SDK Key and Secret in the Video SDK Dashboard, by clicking on the Develop button and selecting Build Video SDK. Make sure you're logged in to your Video SDK account.
ZOOM_SDK_KEY="Your Zoom SDK Key"
ZOOM_SDK_SECRET="Your Zoom SDK Secret"
Nuxt config
We'll add the environment variable to the runtimeConfig in nuxt.config.ts allowing us to access these directly in our application later.
export default defineNuxtConfig({
runtimeConfig: {
ZoomVideoSDKKey: process.env.ZOOM_SDK_KEY,
ZoomVideoSDKSecret: process.env.ZOOM_SDK_SECRET,
},
We'll set compilerOptions to allow for the video-player-container custom element that we'll use with the Zoom Video SDK.
...
vue: {
compilerOptions: {
isCustomElement: (tag) => tag === 'video-player-container'
}
},
We'll add the coi-serviceworker.js script to the app.head.script array to import it in our application. We'll also set the css property to include our custom CSS file.
...
app: {
head: {
script: [ { src: 'coi-serviceworker.js', } ],
}
},
css: ['~/assets/css/main.css'],
})
Step 3: Authenticate a session
The Zoom Video SDK uses JWTs to authenticate a session. We can define an API route in /server/api/token.ts to generate a JWT based on the session slug. Creating a JWT requires the SDK Key and Secret. We'll execute this function only on the server, keeping our SDK key and secret secure. This will create a JWT using the generateSignature function and return it.
export default defineEventHandler(async (event) => {
const query = getQuery(event);
console.log(typeof query.slug);
if (typeof query.slug !== "string") {
throw createError({
statusCode: 400,
statusMessage: "Add a session name as string",
});
}
const JWT = await generateSignature(query.slug, 1);
return JWT;
});
The generateSignature function will use the jsrsasign library and the session name to generate the JWT:
import { KJUR } from "jsrsasign";
function generateSignature(sessionName: string, role: number) {
const runtimeConfig = useRuntimeConfig();
const sdkKey = runtimeConfig.ZoomVideoSDKKey;
const sdkSecret = runtimeConfig.ZoomVideoSDKSecret;
if (!sdkKey || !sdkSecret) {
throw new Error("Missing ZOOM_SDK_KEY or ZOOM_SDK_SECRET");
}
const iat = Math.round(new Date().getTime() / 1000) - 30;
const exp = iat + 60 * 60 * 2;
const oHeader = { alg: "HS256", typ: "JWT" };
const oPayload = {
app_key: sdkKey, tpc: sessionName, role_type: role, version: 1, iat: iat, exp: exp,
};
const sHeader = JSON.stringify(oHeader);
const sPayload = JSON.stringify(oPayload);
const sdkJWT = KJUR.jws.JWS.sign("HS256", sHeader, sPayload, sdkSecret);
return sdkJWT;
}
We can access the ZoomVideoSDKKey and ZoomVideoSDKSecret using the runtimeConfig object.
Step 4: Dynamic route for video chat
We'll create a new file as pages/call/[slug].vue that will create a dynamic route for /call/<slug>. We can use this to create a link for each session based on user input. Users on the same link will be able to join the same session.
In the setup script, we can use the useRoute composable to access the current route and the useFetch composable to fetch the JWT from the /api/token endpoint.
<script setup lang="ts">
const route = useRoute();
const {
data: JWT,
pending,
error,
} = await useFetch(`/api/token?slug=${route.params.slug}`);
</script>
We can then use the JWT and the slug in the template to render the Videocall component. Since the Zoom Video SDK is a client-side SDK and needs access to browser APIs, we'll wrap the Videocall component in a ClientOnly component to ensure that the component is only rendered on the client-side.
...
<template>
<ClientOnly>
<Videocall :slug="route.params.slug" :JWT="JWT" />
</ClientOnly>
</template>
Step 5: Build the videocall component
Now for the fun part of building the video call component. We'll create a new file as components/Videocall.vue that will render the video call UI.
Setup
We'll import the Zoom Video SDK and define the props for the component, including the slug and JWT for authentication. We'll create a username for the user and we'll create a zoom video client with the ZoomVideo.createClient() method.
<script setup lang="ts">
import ZoomVideo, { type VideoPlayer, VideoQuality } from "@zoom/videosdk";
const props = defineProps(['slug', 'JWT'])
const topic = props.slug;
const token = props.JWT;
const username = `User-${String(new Date().getTime()).slice(6)}`;
const client = ZoomVideo.createClient();
We'll create a ref for the video container in the DOM. We'll also create refs for our application state: inSession for rendering the UI based on the current session state and audioMuted & videoMuted for the mute state for camera/microphone. We'll use the onMounted hook to initialize the Video SDK client.
const videoContainer = ref(null);
const inSession = ref(false);
const audioMuted = ref(false);
const videoMuted = ref(false);
onMounted(async () => {
await client.init("en-US", "Global", { patchJsMedia: true });
});
Start a video session
To start a video sessions, we'll define the startCall function. We'll setup an event listener for the peer-video-state-change event, which will be triggered when a new user joins or leaves the session. We'll pass the event payload to the renderVideo function to update the UI based on the new state, we'll define this function later.
We can use the join method to join a video session passing in the topic, token and username as arguments. We can start the audio and video by calling the startVideo and startAudio methods on the mediaStream object.
...
const startCall = async () => {
client.on("peer-video-state-change", renderVideo);
await client.join(topic, token, username);
const mediaStream = client.getMediaStream();
await mediaStream.startAudio();
await mediaStream.startVideo();
We'll call the renderVideo function again after starting the video to update the UI for the local user. Finally, we'll update the application state by setting the inSession ref to true and audioMuted & videoMuted refs to the current state of the camera/microphone.
...
await renderVideo({ action: 'Start', userId: client.getCurrentUserInfo().userId });
inSession.value = true;
audioMuted.value = mediaStream.isAudioMuted();
videoMuted.value = !mediaStream.isCapturingVideo();
};
Rendering videos
We model the renderVideo function to take the event data from the peer-video-state-change event. It'll update the videos in the DOM based on the action from the event. If the action is Stop, we'll remove the video element by calling the detachVideo method and then removing the element from the DOM using the remove method on the element. If the action is Start, we'll add the video to the DOM by first calling the attachVideo method and adding it to the DOM using the appendChild method on the videoContainerRef element.
...
const renderVideo = async (event: { action: "Start" | "Stop"; userId: number; }) => {
const mediaStream = client.getMediaStream();
if (event.action === 'Stop') {
const element = await mediaStream.detachVideo(event.userId);
Array.isArray(element) ? element.forEach((el) => el.remove()) : element.remove();
} else {
const userVideo = await mediaStream.attachVideo(event.userId, VideoQuality.Video_360P);
videoContainer.value?.appendChild(userVideo);
}
};
Leaving the session
Finally, we'll create a function leaveSession to leave the call. We'll remove all the videos from the DOM by calling the detachVideo method on the mediaStream object for each user in the session and then remove the element from the DOM using the remove method on the element. We'll remove the event listener and call the leave method to leave the session, we can then update the application state by setting the inSession ref to false.
...
const leaveCall = async () => {
const mediaStream = client.getMediaStream();
for (const user of client.getAllUser()) {
const element = await mediaStream.detachVideo(user.userId);
Array.isArray(element) ? element.forEach((el) => el.remove()) : element.remove();
}
client.off("peer-video-state-change", renderVideo);
await client.leave();
inSession.value = false;
}
Mute/Unmute buttons
We'll define toggleVideo and toggleAudio functions to mute/unmute the camera/microphone.
We can access the mediaStream object and call the startVideo and stopVideo methods to start/stop the video. We'll update the UI by calling the renderVideo function with the action set to Start or Stop based on the current state of the camera/microphone with the current user's userId. We'll also update the videoMuted ref with the current state of the camera/microphone.
...
const toggleVideo = async () => {
const mediaStream = client.getMediaStream();
if (mediaStream.isCapturingVideo()) {
await mediaStream.stopVideo();
await renderVideo({ action: 'Stop', userId: client.getCurrentUserInfo().userId });
} else {
await mediaStream.startVideo();
await renderVideo({ action: 'Start', userId: client.getCurrentUserInfo().userId });
}
videoMuted.value = !mediaStream.isCapturingVideo();
};
For the toggleAudio function, we can call the unmuteAudio and muteAudio methods on the mediaStream object to mute/unmute the microphone and update the audioMuted ref with the current state of the microphone.
...
const toggleAudio = async () => {
const mediaStream = client.getMediaStream();
if (client.getCurrentUserInfo().muted) {
await mediaStream.unmuteAudio();
} else {
await mediaStream.muteAudio();
}
audioMuted.value = mediaStream.isAudioMuted();
};
</script>
Template for the videocall
We're omitting the styling to make the code more readable, you can check out the styling on GitHub.
We render the video container with the video-player-container tag attaching the videoContainer ref to it. We wrap it in a div with v-show to only display it when the video session is active.
...
<template>
<div v-show="inSession">
<video-player-container
ref="videoContainer"
></video-player-container></div
></template>
We'll render the start call button with the startCall function only when the video session is not active using the v-if directive and the inSession ref. When the session is active, we'll render buttons to mute/unmute the camera/microphone and leave the call. We'll only render the buttons when the video session is active. We'll use the videoMuted and audioMuted refs to render the button text based on the current state of the camera/microphone.
...
</div>
</template>
That's all the code we need to build a video conferencing app using the Zoom Video SDK. You can start a web server and run the app locally by executing npm run dev in the terminal. The app will be available at http://localhost:3000/, you can navigate to /call/<session-name> to join a video call.
Conclusion
We hope this quick guide has given you a good starting point to build your own video conferencing app using the Zoom Video SDK with Vue and Nuxt. We've covered the basics of setting up the project, configuring the app, and building the video conferencing features, you can visit our documentation for adding more features like screen sharing, chat, and recording.
You can also read our blog for the same project built with React and Next.js here. We'll be posting more guides for creating video chat apps with the Zoom Video SDK for different web technologies in the future. Stay tuned for more updates on our blog!
For further community discussion and insight from Zoom Developer Advocates and other developers, please check out the Zoom Developer Forum. For prioritized assistance and troubleshooting, take advantage of Premier Developer Support and submit a ticket.