or
The Zoom Video SDK lets you add real-time audio and video to your application easily, backed by the powerful Zoom technology users love. Let's look at how to integrate the Zoom Video SDK into your react-native app, by building a simple video conferencing application.
Prerequisites:
- Environment setup for React Native
- Node (LTS) & Yarn
- A Zoom Video SDK Account
Step 1: Scaffold the application
To scaffold our application we'll be using the Expo Typescript template. Open a terminal and execute:
$ yarn create expo zoom-video-sdk --template
Select Blank(TypeScript) when asked to "Choose a template".
This will create a new folder called zoom-video-sdk with the following structure:
.
├── App.tsx
├── app.json
├── assets
├── babel.config.js
├── node_modules
├── package.json
├── tsconfig.json
└── yarn.lock
Step 2: Configuring the project
Install the dependencies
We will install the Zoom Video SDK and react-native-pure-jwt. You can use the following command for installation:
$ yarn add @zoom/react-native-videosdk react-native-pure-jwt
We're using v1.10.10 of the Zoom Video SDK, you can find the latest version here.
Add permissions
Add the following to your app.json. If you're not using Expo, you'll have to add these to your native files manually.
{
"expo": {
"android": {
"permissions": ["CAMERA", "RECORD_AUDIO"]
},
"ios": {
"infoPlist": {
"NSCameraUsageDescription": "Text",
"NSMicrophoneUsageDescription": "Text"
}
}
}
}
Install Pods (iOS only)
If you're building for iOS, you'll also need to install cocoapods. Open the Podfile in <your-project-root>/ios/Podfile and add the following line to the post_install block:
...
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings["ONLY_ACTIVE_ARCH"] = "NO"
end
end
...
This is required to install the pods for all architectures. To install the pods, run the following command:
$ npx pod-install
Config file
Finally, let's create a config.ts in the root of our project and add some session details. We'll also add the Zoom App Key and Secret to generate a JWT. You can find these in the Zoom Marketplace in the "Develop" page under the "SDK credentials" section.
export const config = {
sessionName: "DemoSession",
roleType: "1",
sessionPassword: "",
displayName: "Ekaansh",
sessionIdleTimeoutMins: 10,
};
export const ZOOM_APP_KEY = "";
export const ZOOM_APP_SECRET = "";
Disclaimer: To demo the app, we'll be using the Zoom Video SDK credentials to generate a JWT from within the Application. In production use-cases do not store your app key and secret inside your app. This is not a safe operation, instead use a server to generate the tokens and request them in your app instead.
Step 3: Building the app
We can clear out the contents of the App.tsx file and start fresh. Add in the required imports for the project. We'll define generateJwt, usePermission hook and styles later. We'll also import the config we defined before.
import { useRef, useState } from "react";
import { EmitterSubscription, SafeAreaView, Text, View } from "react-native";
import {
EventType,
VideoAspect,
ZoomVideoSdkProvider,
ZoomVideoSdkUser,
ZoomView,
useZoom,
} from "@zoom/react-native-videosdk";
import generateJwt from "./utils/jwt";
import Button, { usePermission } from "./utils/lib";
import { styles } from "./utils/styles";
import { config } from "./config";
Let's create a root component App. We'll call the usePermission hook, it'll request for camera and microphone permissions on Android when the component is mounted. We'll wrap the return with the ZoomVideoSdkProvider, this allows us to use the useZoom hook down the component tree. We'll add in a Call component, let's define it next.
export default function App() {
usePermission();
return (
<ZoomVideoSdkProvider
config={{ appGroupId: "test", domain: "zoom.us", enableLog: true }}
>
<SafeAreaView style={styles.safe}>
<Call />
</SafeAreaView>
</ZoomVideoSdkProvider>
);
}
The Call component
The Call component will contain the videocall logic. We can access the Video SDK methods using the useZoom hook. We'll create a ref listeners to store an array of event subscriptions.
We'll also define state variables - users: to keep track of the users in the call, isInSession: to check if we've joined the call, isAudioMuted: for audio mute status and isVideoMuted: for video mute status.
const Call = () => {
const zoom = useZoom();
const listeners = useRef<EmitterSubscription[]>([]);
const [users, setUsersInSession] = useState<ZoomVideoSdkUser[]>([]);
const [isInSession, setIsInSession] = useState(false);
const [isAudioMuted, setIsAudioMuted] = useState(true);
const [isVideoMuted, setIsVideoMuted] = useState(true);
Let's define a function join to join a video call session. First, we'll access a JWT using our generateJwt function, in production this can be replaced with a network call to your backend.
const join = async () => {
/* Disclaimer: JWT should be generated from your server */
const token = await generateJwt(config.sessionName, config.roleType);
Event listeners
The Video SDK triggers events for various actions like user joining, leaving, video status change, etc. We can listen to these events using the addListener method.
Session Join
The onSessionJoin event is triggered when the user successfully joins a session. We can use this event to access the other users in the session and set the isInSession state to true. We'll append the returned subscription sessionJoin to the listeners ref to access later.
const sessionJoin = zoom.addListener(EventType.onSessionJoin, async () => {
const mySelf = new ZoomVideoSdkUser(await zoom.session.getMySelf());
const remoteUsers = await zoom.session.getRemoteUsers();
setUsersInSession([mySelf, ...remoteUsers]);
setIsInSession(true);
});
listeners.current.push(sessionJoin);
User Join & Leave
The onUserJoin and onUserLeave event is fired when a new remote user joins/leaves the session, we can use this to update the users state to keep track of the users in the session. We'll use this to render the user videos.
const userJoin = zoom.addListener(EventType.onUserJoin, async (event) => {
const { remoteUsers } = event;
const mySelf = await zoom.session.getMySelf();
const remote = remoteUsers.map((user) => new ZoomVideoSdkUser(user));
setUsersInSession([mySelf, ...remote]);
});
listeners.current.push(userJoin);
const userLeave = zoom.addListener(EventType.onUserLeave, async (event) => {
const { remoteUsers } = event;
const mySelf = await zoom.session.getMySelf();
const remote = remoteUsers.map((user) => new ZoomVideoSdkUser(user));
setUsersInSession([mySelf, ...remote]);
});
listeners.current.push(userLeave);
Audio & Video Mute
The onUserVideoStatusChanged and onUserAudioStatusChanged events are fired when any user mutes/unmutes their audio/video. We'll use these to update the local user's mute state with setIsVideoMuted and setIsAudioMuted.
const userVideo = zoom.addListener(
EventType.onUserVideoStatusChanged,
async (event) => {
const { changedUsers } = event;
const mySelf = new ZoomVideoSdkUser(await zoom.session.getMySelf());
changedUsers.find((user) => user.userId === mySelf.userId) &&
mySelf.videoStatus.isOn().then((on) => setIsVideoMuted(!on));
},
);
listeners.current.push(userVideo);
const userAudio = zoom.addListener(
EventType.onUserAudioStatusChanged,
async (event) => {
const { changedUsers } = event;
const mySelf = new ZoomVideoSdkUser(await zoom.session.getMySelf());
changedUsers.find((user) => user.userId === mySelf.userId) &&
mySelf.audioStatus
.isMuted()
.then((muted) => setIsAudioMuted(muted));
},
);
listeners.current.push(userAudio);
Session Leave
The onSessionLeave event is fired when we leave a session, we'll use this event to update the IsInSession state to false, setting the users state to an empty array and removing the event subscription.
const sessionLeave = zoom.addListener(EventType.onSessionLeave, () => {
setIsInSession(false);
setUsersInSession([]);
sessionLeave.remove();
});
After setting up all the listeners we can call the joinSession method to join the session. We'll pass in the sessionName, sessionPassword, userName, and sessionIdleTimeoutMins from the config. We'll pass in the token we generated along with audioOptions & videoOptions.
await zoom
.joinSession({
sessionName: "TestOne",
sessionPassword: "",
userName: "test",
sessionIdleTimeoutMins: 10,
token: token,
audioOptions: { connect: true, mute: true, autoAdjustSpeakerVolume: false },
videoOptions: { localVideoOn: true },
})
.catch((e) => {
console.log(e);
});
};
Leaving the session
To leave the call we'll define the leaveSession function, it will call the leaveSession method on the zoom object to leave the session. We're passing in false to leave the session, you can pass in true instead to end the session. We'll update the isInSessison state to false and remove all the listener subscriptions, and set the ref to an empty array.
const leaveSession = () => {
zoom.leaveSession(false);
setIsInSession(false);
listeners.current.forEach((listener) => listener.remove());
listeners.current = [];
};
Rendering the UI
The Call component will render conditionally based on the isInSession state. If we're in a session, we'll render the videos for each user by mapping over the users state array and passing in their userId to the ZoomView component.
We'll render a Button to leave the session. We'll also render MuteButtons passing in the isAudioMuted and isVideoMuted state, we'll define this component next.
return isInSession ? (
<View style={styles.container}>
{users.map((user) => (
<View style={styles.container} key={user.userId}>
<ZoomView
style={styles.container}
userId={user.userId}
fullScreen
videoAspect={VideoAspect.PanAndScan}
/>
</View>
))}
<MuteButtons isAudioMuted={isAudioMuted} isVideoMuted={isVideoMuted} />
</View>
) : (
If a session isn't active, we'll display some text and render a button to call the join function.
<View style={styles.container}>
<Text style={styles.heading}>Zoom Video SDK</Text>
<Text style={styles.heading}>React Native Quickstart</Text>
<View style={styles.spacer} />
</View>
);
};
MuteButtons component
The MuteButtons component contains the logic to mute/unmute audio and video. We define the onPressAudio function that will call the muteAudio and unmuteAudio methods based on the user's audio state. We can access these using the getMySelf method on the session object.
const MuteButtons = ({ isAudioMuted, isVideoMuted }: { isAudioMuted: boolean; isVideoMuted: boolean }) => {
const zoom = useZoom();
const onPressAudio = async () => {
const mySelf = await zoom.session.getMySelf();
const muted = await mySelf.audioStatus.isMuted();
muted
? await zoom.audioHelper.unmuteAudio(mySelf.userId)
: await zoom.audioHelper.muteAudio(mySelf.userId);
};
We'll do the same thing for the onPressVideo function as well.
const onPressVideo = async () => {
const mySelf = await zoom.session.getMySelf();
const videoOn = await mySelf.videoStatus.isOn();
videoOn
? await zoom.videoHelper.stopVideo()
: await zoom.videoHelper.startVideo();
};
Finally, we can render out two Button components that call these functions onPress.
return (
<View style={styles.buttonHolder}>
<View style={styles.spacer} />
</View>
);
};
That wraps up all the code for the App.tsx file.
Next, let's cover the permissions hook, jwt generation code and styling to complete our application.
Permissions
Here's the code for the usePermission hook we imported earlier, we'll save this as utils/lib.ts.
export async function requestCameraAndAudioPermission() {
try {
const granted = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.CAMERA,
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
]);
if (
granted["android.permission.RECORD_AUDIO"] ===
PermissionsAndroid.RESULTS.GRANTED &&
granted["android.permission.CAMERA"] ===
PermissionsAndroid.RESULTS.GRANTED
) {
console.log("You can use the cameras & mic");
} else {
console.log("Permission denied");
}
} catch (err) {
console.warn(err);
}
}
export const usePermission = () => {
useEffect(() => {
if (Platform.OS === "android") {
requestCameraAndAudioPermission();
}
}, []);
};
JWT generation
Like we mentioned before, in production you should generate the JWT from your server. Here's a sample implementation of the generateJwt function that we're using for this demo in utils/jwt.ts.
import { sign } from 'react-native-pure-jwt';
import { ZOOM_APP_KEY, ZOOM_APP_SECRET } from '../config';
function makeId(length: number) {
var result = '';
var characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
export default async function generateJwt(
sessionName: string,
roleType: string
) {
try {
const token = await sign(
{
app_key: ZOOM_APP_KEY,
version: 1,
user_identity: makeId(10),
iat: new Date().getTime(),
exp: new Date(Date.now() + 23 * 3600 * 1000).getTime(),
tpc: sessionName,
role_type: parseInt(roleType, 10),
cloud_recording_option: 1,
},
ZOOM_APP_SECRET,
{
alg: 'HS256',
}
);
return token;
} catch (e) {
console.log(e);
return '';
}
}
Note: Do not use this in a production application.
Styling
For completeness, here's the styles object we're using in the App.tsx file.
import { StyleSheet } from "react-native";
export const styles = StyleSheet.create({
safe: {
width: "90%",
alignSelf: "center",
margin: 16,
flex: 1,
justifyContent: "center",
},
container: {
width: "100%",
alignSelf: "center",
height: "100%",
flex: 1,
justifyContent: "center",
},
spacer: {
height: 16,
width: 8,
},
heading: {
fontSize: 24,
fontWeight: "bold",
textAlign: "center",
},
button: {
alignItems: "center",
justifyContent: "center",
paddingVertical: 12,
paddingHorizontal: 32,
borderRadius: 4,
elevation: 3,
},
buttonHolder: {
flexDirection: "row",
justifyContent: "center",
margin: 8,
},
text: {
fontSize: 16,
lineHeight: 21,
fontWeight: "bold",
letterSpacing: 0.25,
color: "white",
},
});
Step 4: Running the app
You can run the app using the following command:
npx expo run:ios
# or
npx expo run:android
This will start the Expo development server and open the app in the iOS/Android simulator.
Note: the iOS simulator will not be able to access the camera, you'll see an empty video view. Please use a physical device to demo the video functionality.
Conclusion
Woo-hoo! You've successfully integrated video and audio conferencing into your React Native (Expo) application. This is just the beginning of what you can do with the Video SDK! You can add other features like screen sharing, chat, cloud recording, and live transcriptions. More information is available under the Add Features section in the Zoom Video SDK for React Native documentation.
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.