Skip to main content
Version: 2.x.x

Quick Start for Interactive Live Streaming in React Native

This guide adds interactive live streaming to a React Native app. A live stream supports up to 100 hosts/co-hosts and 2,000 viewers in real-time.

info

For standard live streaming with 6-7 second latency and playback support, follow this documentation.

Prerequisites​

Before proceeding, ensure that your development environment meets the following requirements:

  • Video SDK Developer Account (Not having one, follow Video SDK Dashboard)
  • Basic understanding of React Native
  • React Native VideoSDK
  • Have Node and NPM installed on your device.
  • React Native 0.74 or later
  • 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.
  • Basic understanding of Hooks (useState, useRef, useEffect)
  • React Context API (optional)

Getting Started with the Code!​

Follow the steps to create the environment necessary to add live streaming into your app.

Create new React Native app​

Create a new React Native app using the below command.

npx @react-native-community/cli init videosdkIlsReactNativeApp
cd videosdkIlsReactNativeApp

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 install "@videosdk.live/react-native-sdk@2.0.0" "react-native-safe-area-context"

Project Structure​

Directory Structure
root
├── node_modules
├── android
├── ios
├── App.js
├── api.js
├── index.js

You are going to build the app from functional components. There will be components for users, videos, and controls (mic, camera, leave) over the video.

You will be working on these files:

  • api.js: Responsible for handling API calls such as generating unique streamId and token
  • App.js: Responsible for rendering container and joining the Live Stream.

Project Configuration​

Expo Setup (Essential for Expo Developers)​

If you are developing with Expo, setting up VideoSDK is mandatory. Follow this guide to configure your Expo environment.

Android Setup​

android/app/src/main/AndroidManifest.xml
<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>
note

React Native links the SDK automatically, so settings.gradle and MainApplication need no changes.

android/app/proguard-rules.pro
-keep class videosdk.org.** { *; }
-dontwarn videosdk.org.**

Keep both lines. The -keep rule alone fails the R8 step with a missing-class error.

android/build.gradle
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.

ios/Podfile
platform :ios, '18.0'

target 'videosdkIlsReactNativeApp' do
# ...
pod 'VideoSDKRTC', :podspec => '../node_modules/@videosdk.live/react-native-sdk/ios/VideoSDKRTC.podspec'
end
note

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):

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 streamId. 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.

api.js
// Generate your token at https://app.videosdk.live/api-keys and paste it below.
export const authToken = "";

// API call to create stream
export const createStream = 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 stream: ${res.status}`);
}

//Destructuring the streamId from the response
const { roomId: streamId } = await res.json();
return streamId;
} catch (error) {
console.error("createStream failed", error);
throw error;
}
};
caution

This quick start pastes a temporary token from the VideoSDK Dashboard into the client and lets the user choose the mode. Use this setup only for development. In production, your client must obtain its token from your backend server, and your server must decide whether a participant joins as a host or as an audience member. A token cannot restrict the mode, so any client that holds a valid token can join in SEND_AND_RECV mode. See Authentication and Tokens.

Step 2: Initialize and Join the Live Stream​

To set up the wireframe of App.js, you'll use VideoSDK Hooks and Context Providers, which include:

  • RoomProvider: The context provider for managing live streams. Accepts config and token as props and makes them accessible to all nested components.
  • useRoom: A hook providing APIs to manage live streams, like joining, leaving, and toggling the mic or webcam.
  • useParticipant: A hook to handle properties and events for a specific participant, such as their name, webcam, and mic streams.

Before proceeding, let's understand the two modes of a Live Stream:

1. SEND_AND_RECV (For Host or Co-host):​
  • Designed primarily for the Host or Co-host.
  • Allows sending and receiving media.
  • Hosts can broadcast their audio/video and interact directly with the audience.
2. RECV_ONLY (For Audience):​
  • Tailored for the Audience.
  • Enables receiving media shared by the Host.
  • Audience members can view and listen but cannot share their own media.
ILS Mode Demonstration

The Join Screen allows users to either create a new Live Stream or join an existing one as a host or audience, using these actions:

  1. Join as Host: Allows the user to join an existing Live Stream using the provided streamId with the SEND_AND_RECV mode, enabling full host privileges.

  2. Join as Audience: Enables the user to join an existing Live Stream using the provided streamId with the RECV_ONLY mode, allowing view-only access.

  3. Create Live Stream as Host: Lets the user initiate a new Live Stream in SEND_AND_RECV mode, granting full host controls.

On Android, the SDK does not ask for camera and microphone access, so the two host actions first ask with requestPermission() from useMediaDevice.

App.js
import React, { useState } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
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 { authToken, createStream } from "./api";

// Join Screen - Handles joining or creating a live stream
function JoinView({ initializeStream, setMode }) {
const [streamId, setStreamId] = useState("");
const { requestPermission } = useMediaDevice();

const handleAction = async (mode, id) => {
// On Android, the SDK does not ask for camera and microphone access,
// so ask before joining as a host
if (mode === Constants.Mode.SEND_AND_RECV) {
await requestPermission(Constants.permission.AUDIO_AND_VIDEO);
}
// Sets the mode (Host or Audience) and initializes the stream
setMode(mode);
await initializeStream(id);
};

return (
<SafeAreaView style={styles.container}>
<TouchableOpacity
style={styles.button}
onPress={() => handleAction(Constants.Mode.SEND_AND_RECV, null)}
>
<Text style={styles.buttonText}>Create Live Stream as Host</Text>
</TouchableOpacity>
<Text style={styles.separatorText}>---------- OR ----------</Text>
<TextInput
style={styles.input}
placeholder="Enter Stream Id"
onChangeText={setStreamId}
/>
<TouchableOpacity
style={styles.button}
onPress={() => handleAction(Constants.Mode.SEND_AND_RECV, streamId)}
>
<Text style={styles.buttonText}>Join as Host</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.button}
onPress={() => handleAction(Constants.Mode.RECV_ONLY, streamId)}
>
<Text style={styles.buttonText}>Join as Audience</Text>
</TouchableOpacity>
</SafeAreaView>
);
}

// Live Stream Container - Placeholder for the live stream container
function LSContainer(props) {
return null;
}

// Main App Component - Handles the app flow and stream lifecycle
function AppContent() {
const [streamId, setStreamId] = useState(null); // Holds the current stream ID
// Holds the current user mode (Host or Audience)
const [mode, setMode] = useState(
/** @type {"SEND_AND_RECV" | "RECV_ONLY"} */ (Constants.Mode.SEND_AND_RECV)
);

const initializeStream = async (id) => {
if (!authToken) {
console.error("PLEASE PROVIDE TOKEN IN api.js FROM app.videosdk.live");
return;
}
// Creates a new stream if no ID is provided or uses the given stream ID
const newStreamId = id || (await createStream({ token: authToken }));
setStreamId(newStreamId);
};

const onStreamLeave = () => setStreamId(null); // Resets the stream state on leave

return authToken && streamId ? (
<RoomProvider
config={{
roomId: streamId,
micEnabled: true, // Enables microphone by default
camEnabled: true, // Enables webcam by default
participantName: "John Doe", // Default participant name
mode,
defaultCamera: "front",
multiStream: true, // Sends several quality layers, so viewers on slow networks get a lower one
}}
token={authToken}
>
{/* Renders the live stream container if a stream is active */}
<LSContainer streamId={streamId} onLeave={onStreamLeave} mode={mode} />
</RoomProvider>
) : (
// Renders the join view if no stream is active
<JoinView initializeStream={initializeStream} setMode={setMode} />
);
}

function App() {
return (
<SafeAreaProvider>
<AppContent />
</SafeAreaProvider>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#F6F6FF",
justifyContent: "center",
paddingHorizontal: 12,
},
button: {
backgroundColor: "#1178F8",
padding: 12,
marginTop: 14,
borderRadius: 6,
alignItems: "center",
},
buttonText: {
color: "white",
fontSize: 16,
},
input: {
padding: 12,
borderWidth: 1,
borderRadius: 6,
fontStyle: "italic",
marginVertical: 10,
},
controls: {
flexDirection: "row",
justifyContent: "space-around",
marginVertical: 16,
flexWrap: "wrap",
},
noMedia: {
backgroundColor: "grey",
height: 300,
justifyContent: "center",
alignItems: "center",
marginVertical: 8,
marginHorizontal: 8,
},
noMediaText: {
fontSize: 16,
},
separatorText: {
alignSelf: "center",
fontSize: 22,
marginVertical: 16,
fontStyle: "italic",
color: "grey",
},
});

export default App;

Output​

Step 3: Setting Up Live Stream View and Container​

This step implements the Live Stream View and Container components to manage and display the hosts:

  1. StreamView Component:

    • Lists the participants and renders each one with the Participant component, which shows only participants who publish audio or video (the hosts).
    • Includes LSControls for managing the session.
  2. LSContainer Component:

    • Manages joining the stream via join from useRoom.
    • Keeps the local participant's mode in state and passes it to LSControls through StreamView.
    • Shows the StreamView after joining or a Join Stream button otherwise.
note

Do not call join() from a mount effect inside RoomProvider. It fails with 4002 INVALID_TOKEN even when the token is valid, and a click on a previous screen does not make a mount effect safe. Call join() from a user action inside RoomProvider, or set joinWithoutUserInteraction.

Use exactly one of these two — setting the flag and calling join() together can create two room instances.

  • There is a button inside RoomProvider. Call join() 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 joinWithoutUserInteraction to true as a prop on RoomProvider, not a key inside config, and do not call join() at all.

With the flag set, the SDK makes the join() call itself, so there is no promise of yours to wrap in try / catch — the error handling in the sample does not apply. Subscribe to onError from useRoom to catch a failed join and read its code. For a host, ask for camera and microphone access with requestPermission() before you render RoomProvider, because on Android the SDK does not ask for it.

App.js
// Component to manage live stream container and session joining
function LSContainer({ streamId, onLeave, mode }) {
const [joined, setJoined] = useState(false); // Track if the user has joined the stream
// The local participant's mode; LSControls updates it after changeMode() resolves
const [currentMode, setCurrentMode] = useState(mode);

const { join } = useRoom({
onRoomJoined: () => setJoined(true), // Set `joined` to true when successfully joined
onRoomLeft: onLeave, // Handle the leave stream event
onError: (error) => Alert.alert("Error", error.message), // Display an alert on encountering an error
});

const handleJoin = async () => {
try {
await join();
} catch (error) {
console.error("Failed to join stream", error);
}
};

return (
<SafeAreaView style={styles.container}>
{streamId ? (
<Text style={{ fontSize: 18, padding: 12 }}>Stream Id :{streamId}</Text>
) : null}
{/* Show the stream view if joined, otherwise display the "Join Stream" button */}
{joined ? (
<StreamView currentMode={currentMode} setCurrentMode={setCurrentMode} />
) : (
<TouchableOpacity style={styles.button} onPress={handleJoin}>
<Text style={styles.buttonText}>Join Stream</Text>
</TouchableOpacity>
)}
</SafeAreaView>
);
}

// Component to display the live stream view
function StreamView({ currentMode, setCurrentMode }) {
const { participants } = useRoom(); // Access participants using the VideoSDK useRoom hook
const participantsArrId = [...participants.keys()];

return (
<View style={{ flex: 1 }}>
<FlatList
data={participantsArrId}
renderItem={({ item }) => {
return <Participant participantId={item} />;
}}
/>
<LSControls currentMode={currentMode} setCurrentMode={setCurrentMode} />
</View>
);
}

function Participant(props) {
return null;
}

function LSControls(props) {
return null;
}

Output​

Step 4: Render Individual Participant's View​

This step component displays an individual participant's video. It uses the useParticipant hook to retrieve the participant's data.

It renders only participants whose camera or microphone is on. Audience members join in RECV_ONLY mode and publish nothing, so they get no tile.

We don't need to manage the audio stream manually; RTCView shows video only, and the SDK plays remote audio for you.

App.js
// Component to render video streams for a participant
function Participant({ participantId }) {
const { webcamStream, webcamOn, micOn } = useParticipant(participantId);

// Audience members publish no audio or video, so show only participants who do
if (!webcamOn && !micOn) {
return null;
}

return webcamOn && webcamStream ? (
<RTCView
streamURL={new MediaStream([webcamStream.track]).toURL()}
objectFit={"cover"}
style={{
height: 300,
marginVertical: 8,
marginHorizontal: 8,
}}
/>
) : (
<View style={styles.noMedia}>
<Text style={styles.noMediaText}>NO MEDIA</Text>
</View>
);
}

Output​

Step 5: Implement Live Stream Controls​

This step provides buttons to leave the stream, toggle the microphone and camera, and switch between Host Mode (send/receive streams) and Audience Mode (receive streams only). It uses useRoom to access these functionalities.

These methods are asynchronous and return a Promise, so await them and handle failures with try / catch. On Android, the SDK does not ask for camera and microphone access, so LSControls asks with requestPermission() before it switches an audience member to Host Mode.

LSControls gets the current mode from LSContainer, which updates it after changeMode() resolves, so the buttons follow the switch.

App.js
// Component for managing stream controls
function LSControls({ currentMode, setCurrentMode }) {
const {
leave,
toggleMic,
toggleCam,
changeMode,
} = useRoom(); // Access methods
const { requestPermission } = useMediaDevice();

const handleLeave = async () => {
try {
await leave();
} catch (error) {
console.error("Failed to leave stream", error);
}
};

const handleToggleMic = async () => {
try {
await toggleMic();
} catch (error) {
console.error("Failed to toggle mic", error);
}
};

const handleToggleWebcam = async () => {
try {
await toggleCam();
} catch (error) {
console.error("Failed to toggle webcam", error);
}
};

const handleChangeMode = async () => {
const nextMode =
currentMode === Constants.Mode.SEND_AND_RECV ? "RECV_ONLY" : "SEND_AND_RECV";
try {
if (nextMode === Constants.Mode.SEND_AND_RECV) {
await requestPermission(Constants.permission.AUDIO_AND_VIDEO);
}
await changeMode(nextMode);
setCurrentMode(nextMode);
} catch (error) {
console.error("Failed to change mode", error);
}
};

return (
<View style={styles.controls}>
<TouchableOpacity style={styles.button} onPress={handleLeave}>
<Text style={styles.buttonText}>Leave</Text>
</TouchableOpacity>
{currentMode === Constants.Mode.SEND_AND_RECV && (
<>
<TouchableOpacity style={styles.button} onPress={handleToggleMic}>
<Text style={styles.buttonText}>Toggle Mic</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={handleToggleWebcam}>
<Text style={styles.buttonText}>Toggle Camera</Text>
</TouchableOpacity>
</>
)}
<TouchableOpacity style={styles.button} onPress={handleChangeMode}>
<Text style={styles.buttonText}>
{currentMode === Constants.Mode.SEND_AND_RECV
? "Switch to Audience"
: "Switch to Host"}
</Text>
</TouchableOpacity>
</View>
);
}

Output​

Final Output​

Got a Question? Ask us on discord