Quick Start for Conference in React Native
This guide adds video and audio calling to a React Native app.
In this quickstart, you'll explore the video and audio calling feature of VideoSDK. Follow the step-by-step guide to integrate it within your application.
Prerequisites
- Node.js >= 22.11.0
- React Native 0.74 or later
- NPM v10+ (comes installed with newer Node versions)
- Android Studio or Xcode installed
- For Android, a device or emulator running Android 7.0 (API level 24) or later.
- For iOS, an iPhone or iPad running iOS 18.0 or later.
App Architecture
The app will contain two screens:
-
Join Screen: This screen allows users to either create a room or join a predefined room. -
Room Screen: This screen contains a participant list and room controls, such as enabling/disabling the microphone and camera, and leaving the room.

Getting Started with the Code!
Create App
Create a new React Native App using the below command.
npx @react-native-community/cli init AppName
For React Native setup, you can follow the Official Documentation.
Install VideoSDK and Dependencies
Install the VideoSDK along with react-native-safe-area-context, which is used to render the app within the device's safe area. Ensure that you are in your project directory before running this command.
- NPM
- Yarn
npm install "@videosdk.live/react-native-sdk@2.0.0" "react-native-safe-area-context"
yarn add "@videosdk.live/react-native-sdk@2.0.0" "react-native-safe-area-context"
Project Structure
root
├── node_modules
├── android
├── ios
├── App.js
├── api.js
├── index.js
Project Configuration
Android Setup
Without it, the Android build cannot resolve the SDK's Android library.
allprojects {
repositories {
maven { url "https://central.sonatype.com/repository/maven-snapshots/" }
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
</manifest>
React Native links the SDK automatically, so settings.gradle and MainApplication need no changes.
-keep class videosdk.org.** { *; }
-dontwarn videosdk.org.**
Keep both lines. The -keep rule alone fails the R8 step with a missing-class error.
buildscript {
ext {
minSdkVersion = 24
}
}
iOS Setup
To update CocoaPods, you can reinstall the gem using the following command:
sudo gem install cocoapods
Add the VideoSDKRTC pod inside your app's target block in the Podfile, and raise the platform to iOS 18.0.
platform :ios, '18.0'
target 'AppName' do
# ...
pod 'VideoSDKRTC', :podspec => '../node_modules/@videosdk.live/react-native-sdk/ios/VideoSDKRTC.podspec'
end
Set the iOS Deployment Target of your app target to 18.0 in Xcode as well. The SDK's own pod is linked automatically.
After updating the Podfile, you need to install the pods by running the following command:
pod install
Add the following lines to your info.plist file located at (project folder/ios/projectname/info.plist):
<key>NSCameraUsageDescription</key>
<string>Camera permission description</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone permission description</string>
Step 1: Get started with API.js
Prior to moving on, you must create an API request to generate a unique roomId. You will need an authentication token, which you can create either through the videosdk-rtc-api-server-examples or directly from the VideoSDK Dashboard for developers.
// Generate your token at https://app.videosdk.live/api-keys and paste it below.
export const token = "";
// API call to create room
export const createRoom = async ({ token }) => {
try {
const res = await fetch(`https://api.videosdk.live/v2/rooms`, {
method: "POST",
headers: {
authorization: `${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
if (!res.ok) {
throw new Error(`Failed to create room: ${res.status}`);
}
const { roomId } = await res.json();
return roomId;
} catch (error) {
console.error("createRoom failed", error);
throw error;
}
};
Step 2: Wireframe App.js with all the components
To build the wireframe of App.js, you need to use VideoSDK Hooks and Context Providers. VideoSDK provides RoomProvider, RoomConsumer, useRoom and useParticipant hooks.
First you need to understand Context Provider and Consumer. Context is primarily used when some data needs to be accessible by many components at different nesting levels.
- RoomProvider: This is the Context Provider. It accepts
configandtokenas props, and passes the room to every component inside it. One Provider can be connected to many consumers. Providers can be nested to override values deeper within the tree. - RoomConsumer: This is the Context Consumer. All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes.
- useRoom: This is the room hook API. It includes all the information related to room such as join, leave, toggle mic or webcam etc.
- useParticipant: This is the participant hook API. It is responsible for handling all the events and props related to one particular participant such as name, webcamStream, micStream etc.
The Room Context provides a way to listen for any changes that occur when a participant joins the room or makes modifications to their microphone, camera, and other settings.
Begin by making a few changes to the code in the App.js file.
import React, { useState } from "react";
import {
TouchableOpacity,
Text,
TextInput,
View,
FlatList,
} from "react-native";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import {
RoomProvider,
useRoom,
useParticipant,
useMediaDevice,
Constants,
MediaStream,
RTCView,
} from "@videosdk.live/react-native-sdk";
import { createRoom, token } from "./api";
function JoinScreen(props) {
return null;
}
function ControlsContainer() {
return null;
}
function RoomView() {
return null;
}
function AppContent() {
const [roomId, setRoomId] = useState(null);
const getRoomId = async (id) => {
if (!token) {
console.error("PLEASE PROVIDE TOKEN IN api.js FROM app.videosdk.live");
return;
}
const roomId = id == null ? await createRoom({ token }) : id;
setRoomId(roomId);
};
return roomId ? (
<SafeAreaView style={{ flex: 1, backgroundColor: "#F6F6FF" }}>
<RoomProvider
config={{
roomId,
micEnabled: false,
camEnabled: true,
participantName: "Test User",
defaultCamera: "front",
}}
token={token}
>
<RoomView />
</RoomProvider>
</SafeAreaView>
) : (
<JoinScreen getRoomId={getRoomId} />
);
}
export default function App() {
return (
<SafeAreaProvider>
<AppContent />
</SafeAreaProvider>
);
}
Step 3: Implement Join Screen
The join screen lets the user either create a new room or join an existing one.
function JoinScreen(props) {
const [roomVal, setRoomVal] = useState("");
return (
<SafeAreaView
style={{
flex: 1,
backgroundColor: "#F6F6FF",
justifyContent: "center",
paddingHorizontal: 6 * 10,
}}
>
<TouchableOpacity
onPress={() => {
props.getRoomId();
}}
style={{ backgroundColor: "#1178F8", padding: 12, borderRadius: 6 }}
>
<Text style={{ color: "white", alignSelf: "center", fontSize: 18 }}>
Create Room
</Text>
</TouchableOpacity>
<Text
style={{
alignSelf: "center",
fontSize: 22,
marginVertical: 16,
fontStyle: "italic",
color: "grey",
}}
>
---------- OR ----------
</Text>
<TextInput
value={roomVal}
onChangeText={setRoomVal}
placeholder={"XXXX-XXXX-XXXX"}
style={{
padding: 12,
borderWidth: 1,
borderRadius: 6,
fontStyle: "italic",
}}
/>
<TouchableOpacity
style={{
backgroundColor: "#1178F8",
padding: 12,
marginTop: 14,
borderRadius: 6,
}}
onPress={() => {
props.getRoomId(roomVal);
}}
>
<Text style={{ color: "white", alignSelf: "center", fontSize: 18 }}>
Join Room
</Text>
</TouchableOpacity>
</SafeAreaView>
);
}
Output

Step 4: Implement Controls
Next step is to create a ControlsContainer component to manage features such as Join or Leave Room and Enable or Disable Webcam/Mic.
In this step, the useRoom hook is utilized to acquire all the required methods such as join(), leave(), toggleCam() and toggleMic(). toggleCam() turns the camera off when it is on and on when it is off; toggleMic() does the same for the mic.
These methods are asynchronous and return a Promise, so await them and handle failures with try / catch. Some errors, such as a camera that cannot be opened, are reported only on the onError callback of useRoom, so RoomView subscribes to it as well.
On Android, the SDK does not ask the user for camera and microphone access. ControlsContainer therefore asks with requestPermission() from useMediaDevice before it calls join().
Do not call join() from a mount effect inside RoomProvider. React runs that effect before RoomProvider sets the token, so join() fails with 4002 INVALID_TOKEN even when the token itself is valid. What matters is where the call runs, not what the user clicked to get there: a click on a previous screen does not make a mount effect safe.
Use exactly one of these two — setting the flag and calling join() together
can create two room instances.
- There is a button inside
RoomProvider. Calljoin()from its click handler: from the event, never from an effect. - There is no button, because the user already acted on an earlier screen — a
device check, a room link, a button elsewhere in your app. Set
joinWithoutUserInteractiontotrueas a prop onRoomProvider, not a key insideconfig, and do not calljoin()at all.
function RoomScreen({ roomId, token }) {
return (
<RoomProvider
config={{
roomId,
participantName: "Participant",
micEnabled: true,
camEnabled: true,
}}
token={token}
// The user already acted on an earlier screen, so let the SDK join.
// This is a prop on RoomProvider, not a key inside config.
joinWithoutUserInteraction={true}
>
<RoomView />
</RoomProvider>
);
}
function RoomView() {
const [status, setStatus] = useState("Joining...");
// Do not call join() here, and do not call it from a useEffect:
// the effect runs before RoomProvider has configured the token,
// and the join fails with 4002 INVALID_TOKEN.
useRoom({
onRoomJoined: () => setStatus("Joined"),
// With the flag set there is no join() promise to catch, so a failed
// join arrives here.
onError: ({ code, message }) => setStatus(`Join failed (${code}): ${message}`),
});
return (
<View>
<Text>{status}</Text>
</View>
);
}
With the flag set, the SDK makes the join() call itself, so there is no
promise of yours to wrap in try / catch — the try / catch around
join() in ControlsContainer below does not apply. Subscribe to onError
from useRoom, as the sample above does, to catch a failed join and read its
code. Ask for camera and microphone access with requestPermission() before you
render RoomProvider, because on Android the SDK does not ask for it.
const Button = ({ onPress, buttonText, backgroundColor }) => {
return (
<TouchableOpacity
onPress={onPress}
style={{
backgroundColor: backgroundColor,
justifyContent: "center",
alignItems: "center",
padding: 12,
borderRadius: 4,
}}
>
<Text style={{ color: "white", fontSize: 12 }}>{buttonText}</Text>
</TouchableOpacity>
);
};
function ControlsContainer({ join, leave }) {
const { requestPermission } = useMediaDevice();
const { toggleCam, toggleMic } = useRoom();
const handleJoin = async () => {
try {
// On Android, the SDK does not ask for camera and microphone access
await requestPermission(Constants.permission.AUDIO_AND_VIDEO);
await join();
} catch (error) {
console.error("Failed to join room", error);
}
};
const handleToggleWebcam = async () => {
try {
await toggleCam();
} catch (error) {
console.error("Failed to toggle webcam", error);
}
};
const handleToggleMic = async () => {
try {
await toggleMic();
} catch (error) {
console.error("Failed to toggle mic", error);
}
};
const handleLeave = async () => {
try {
await leave();
} catch (error) {
console.error("Failed to leave room", error);
}
};
return (
<View
style={{
padding: 24,
flexDirection: "row",
justifyContent: "space-between",
}}
>
<Button
onPress={handleJoin}
buttonText={"Join"}
backgroundColor={"#1178F8"}
/>
<Button
onPress={handleToggleWebcam}
buttonText={"Toggle Webcam"}
backgroundColor={"#1178F8"}
/>
<Button
onPress={handleToggleMic}
buttonText={"Toggle Mic"}
backgroundColor={"#1178F8"}
/>
<Button
onPress={handleLeave}
buttonText={"Leave"}
backgroundColor={"#FF0000"}
/>
</View>
);
}
function ParticipantList() {
return null;
}
function RoomView() {
const { join, leave, roomId } = useRoom({
// Some errors are reported only here, not to the try / catch around each call
onError: ({ code, message }) => {
console.error("Room error", code, message);
},
});
return (
<View style={{ flex: 1 }}>
{roomId ? (
<Text style={{ fontSize: 18, padding: 12 }}>
Room Id :{roomId}
</Text>
) : null}
<ParticipantList />
<ControlsContainer
join={join}
leave={leave}
/>
</View>
);
}
Output

Step 5: Render Participant List
After implementing the controls, next step is to render the joined participants.
You can get all the joined participants from the useRoom Hook.
function ParticipantView(props) {
return null;
}
function ParticipantList({ participants }) {
return participants.length > 0 ? (
<FlatList
data={participants}
renderItem={({ item }) => {
return <ParticipantView participantId={item} />;
}}
/>
) : (
<View
style={{
flex: 1,
backgroundColor: "#F6F6FF",
justifyContent: "center",
alignItems: "center",
}}
>
<Text style={{ fontSize: 20 }}>Press Join button to enter room.</Text>
</View>
);
}
function RoomView() {
// Get `participants` from useRoom Hook
const { join, leave, participants, roomId } =
useRoom({
onError: ({ code, message }) => {
console.error("Room error", code, message);
},
});
const participantsArrId = [...participants.keys()];
return (
<View style={{ flex: 1 }}>
{roomId ? (
<Text style={{ fontSize: 18, padding: 12 }}>
Room Id :{roomId}
</Text>
) : null}
<ParticipantList participants={participantsArrId} />
<ControlsContainer
join={join}
leave={leave}
/>
</View>
);
}
Step 6: Handling Participant's Media
Before Handling the Participant's Media, you need to understand a couple of concepts.
1. useParticipant Hook
The useParticipant hook is responsible for handling all the properties and events of one particular participant joined in the room. It will take participantId as argument.
const { webcamStream, webcamOn, displayName } = useParticipant(participantId);
2. Rendering a stream
webcamStream is a stream object, and its track property holds the video. Wrap the track in the SDK's MediaStream and pass the URL from its toURL() method to the RTCView component. RTCView shows video only; the SDK plays remote audio for you, without a view.
<RTCView
streamURL={new MediaStream([webcamStream.track]).toURL()}
objectFit={"cover"}
style={{
height: 300,
marginVertical: 8,
marginHorizontal: 8,
}}
/>
Now you can use the hook and the API to create ParticipantView
function ParticipantView({ participantId }) {
const { webcamStream, webcamOn } = useParticipant(participantId);
return webcamOn && webcamStream ? (
<RTCView
streamURL={new MediaStream([webcamStream.track]).toURL()}
objectFit={"cover"}
style={{
height: 300,
marginVertical: 8,
marginHorizontal: 8,
}}
/>
) : (
<View
style={{
backgroundColor: "grey",
height: 300,
justifyContent: "center",
alignItems: "center",
marginVertical: 8,
marginHorizontal: 8,
}}
>
<Text style={{ fontSize: 16 }}>NO MEDIA</Text>
</View>
);
}
Output

Expo user, follow the Expo setup to configure your project.
Run your application
npm run android # Android
npm run ios # iOS
Got a Question? Ask us on discord

